OpenKitx403
HTTP-native wallet authentication for Solana
OpenKitx403 is an open-source, TypeScript-first protocol that standardizes HTTP 403 as the semantic "prove you control this wallet" challenge for Solana wallet authentication.
Features
- HTTP-native: Uses standard HTTP 403 challenges, no custom protocols
- Stateless: No required server-side sessions or JWTs
- Secure:
ed25519-solanasignatures with replay protection - Production-ready: Full TypeScript client + Python server/client SDKs
- AI-friendly: Agent SDK and LangChain tools
- Token-gating ready: Built-in support for NFT/token requirements
- Multi-stack: Browser, Node, FastAPI, and autonomous agents
Why OpenKitx403?
Traditional web authentication requires centralized identity providers or long-lived sessions. OpenKitx403 enables stateless, cryptographic wallet-based authentication using standard HTTP semantics. It works with Phantom, Backpack, and Solflare wallets and is designed to be implemented across multiple languages.
Core Idea
Instead of "login with email/password", your API says:
403 Forbidden
WWW-Authenticate: OpenKitx403 realm="api-v1", version="1", challenge="<base64url>"
The client signs that challenge with a Solana wallet and re-sends the request with:
Authorization: OpenKitx403 addr="<wallet>", sig="<signature>", challenge="<base64url>", ts="<iso8601>", nonce="<nonce>", bind="<method:path>"
The server verifies the signature, enforces bindings, and grants access.
How It Works
OpenKitx403 follows a simple challenge-response authentication flow:
Client Request
Client attempts to access a protected resource.
GET /protected
Server Challenge
Server responds with 403 and a challenge.
WWW-Authenticate: OpenKitx403 ...
User Signs
Wallet signs the challenge using ed25519-solana.
Ed25519 signature
Client Retry
Client retries with an Authorization: OpenKitx403 ... header.
Success
Server verifies and returns 200 OK.
No Sessions, No JWTs, No OAuth
OpenKitx403 is stateless by design. Each request is authenticated via a signed challenge, with replay protection and optional token-gating on top.
Quick Start
Get running in 5 minutes.
1. Install Packages
# TypeScript client (browser/Node)
npm install @openkitx403/client
# Python server (FastAPI)
pip install openkitx403
# Python client (scripts/agents)
pip install openkitx403-client
# Agent SDK (TypeScript + Python)
npm install @openkitx403/agent
pip install openkitx403-agent
2. Minimal FastAPI Server
from fastapi import FastAPI, Depends
from openkitx403 import OpenKit403Middleware, require_openkitx403_user
app = FastAPI(title="My Wallet-Protected API")
app.add_middleware(
OpenKit403Middleware,
audience="https://api.example.com",
issuer="my-api-v1",
ttl_seconds=60,
clock_skew_seconds=120,
bind_method_path=True,
origin_binding=False,
ua_binding=False,
replay_backend="memory",
)
@app.get("/protected")
async def protected(user = Depends(require_openkitx403_user)):
return {
"message": f"Authenticated as {user.address}",
"wallet": user.address,
}
3. Minimal Browser Client
import { OpenKit403Client } from "@openkitx403/client";
const client = new OpenKit403Client();
// Connect wallet
await client.connect("phantom");
// Authenticate
const response = await client.authenticate({
resource: "https://api.example.com/protected",
method: "GET",
});
if (response.ok) {
const data = await response.json();
console.log("✅ Authenticated as:", client.getAddress());
console.log("Data:", data);
} else {
console.error("❌ Authentication failed:", response.status);
}
Installation
TypeScript Client
For browser and Node.js applications:
npm install @openkitx403/client
Supports:
- Browser (Phantom, Backpack, Solflare wallets)
- Node.js (with Solana keypair)
- React, Vue, Svelte, etc.
Python Server (FastAPI)
For FastAPI applications:
pip install openkitx403
# or
poetry add openkitx403
Includes:
- FastAPI middleware (
OpenKit403Middleware) - Replay protection store (memory backend)
- CORS helpers for
WWW-Authenticate
Python Client
For Python scripts, agents and automation:
pip install openkitx403-client
# or
poetry add openkitx403-client
Includes:
- Full 403 → challenge → sign → retry flow
- Solana keypair support via
solders - Clean error types (
OpenKit403ClientError)
Agent SDK
For AI agents and LangChain workflows:
npm install @openkitx403/agent
pip install openkitx403-agent
Includes:
- High-level
OpenKit403Agentfor autonomous flows - LangChain tool integration
- Helpers for resource-based authentication
Client SDK
Browser Usage
Using OpenKitx403 in the browser with wallet extensions:
import { OpenKit403Client, detectWallets } from "@openkitx403/client";
const client = new OpenKit403Client();
const wallets = await detectWallets();
console.log("Detected wallets:", wallets);
// 1. Connect a wallet
await client.connect("phantom");
// 2. Authenticate with API
const response = await client.authenticate({
resource: "https://api.example.com/protected",
method: "GET",
});
if (response.ok) {
console.log("Authenticated as:", client.getAddress());
const data = await response.json();
console.log("API response:", data);
} else {
console.error("Auth failed:", response.status);
}
React Example
import { useState } from "react";
import { OpenKit403Client } from "@openkitx403/client";
export function App() {
const [client] = useState(() => new OpenKit403Client());
const [address, setAddress] = useState<string | null>(null);
const login = async () => {
await client.connect("phantom");
const response = await client.authenticate({
resource: "https://api.example.com/user/profile",
});
if (response.ok) {
setAddress(client.getAddress());
}
};
return (
<div>
{!address ? (
<button onClick={login}>Connect Wallet</button>
) : (
<p>Connected: {address}</p>
)}
</div>
);
}
Node.js Usage
For Node.js scripts you can either use the browser-compatible client with a custom wallet adapter, or use the Python client from automation. Example using Python client:
from solders.keypair import Keypair
from openkitx403_client import OpenKit403Client
keypair = Keypair()
print("Wallet:", keypair.pubkey())
client = OpenKit403Client(keypair)
response = client.authenticate(
url="https://api.example.com/protected",
method="GET",
)
if response.ok:
print(response.json())
Server SDK
FastAPI Middleware
from typing import Optional, List, Callable
from fastapi import FastAPI, Depends
from openkitx403 import (
OpenKit403Middleware,
require_openkitx403_user,
get_openkitx403_user,
)
app = FastAPI()
app.add_middleware(
OpenKit403Middleware,
audience="https://api.example.com",
issuer="my-api-v1",
ttl_seconds=60,
clock_skew_seconds=120,
bind_method_path=True,
origin_binding=False,
ua_binding=False,
replay_backend="memory",
excluded_paths=["/health", "/docs"],
)
@app.get("/protected")
async def protected(user = Depends(require_openkitx403_user)):
return {"address": user.address}
@app.get("/optional")
async def optional(user = Depends(get_openkitx403_user)):
if user:
return {"message": f"Authenticated as {user.address}"}
return {"message": "Anonymous"}
Protocol Specification (Summary)
Scheme & Version
- Scheme:
OpenKitx403 - Version:
1 - Algorithm:
ed25519-solana
WWW-Authenticate Header (Server → Client)
WWW-Authenticate: OpenKitx403 realm="api-v1", version="1", challenge="<base64url(challenge_json)>"
Challenge JSON
| Field | Description |
|---|---|
v |
Protocol version (must be 1) |
alg |
Algorithm (must be ed25519-solana) |
aud |
Audience / expected API origin |
iss |
Issuer / server identifier |
nonce |
Random single-use nonce |
iat |
Issued-at timestamp (ISO 8601) |
exp |
Expiration timestamp |
method |
HTTP method (if method/path binding enabled) |
path |
HTTP path (if method/path binding enabled) |
uaBind |
Require User-Agent binding |
originBind |
Require Origin binding |
ext |
Extension data for custom requirements |
Signing String
OpenKitx403 Challenge
domain: https://api.example.com
server: api-v1
nonce: E2o6p0q0Zl5PBjXc
ts: 2025-11-05T10:30:00Z
method: GET
path: /protected
payload: {"v":1,"alg":"ed25519-solana","aud":"https://api.example.com",...}
This string is signed with Ed25519 using the wallet's private key.
LangChain & Agent SDK
TypeScript Agent
import { OpenKit403Agent } from "@openkitx403/agent";
const agent = new OpenKit403Agent({
wallet: "phantom",
autoConnect: true,
});
const result = await agent.execute({
resource: "https://api.example.com/protected",
});
console.log(result);
LangChain (TypeScript)
import { SolanaWalletAuthTool } from "@openkitx403/agent";
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ temperature: 0 });
const tools = [new SolanaWalletAuthTool("phantom")];
const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentType: "openai-functions",
});
const result = await executor.invoke({
input: "Connect my Phantom wallet and fetch my profile from https://api.example.com/profile",
});
Python Agent
from solders.keypair import Keypair
from openkitx403_agent import OpenKit403Agent, AgentExecuteOptions
keypair = Keypair()
agent = OpenKit403Agent(keypair, auto_connect=True)
result = await agent.execute(
AgentExecuteOptions(
resource="https://api.example.com/protected",
method="GET",
)
)
print(result)
Security Model
OpenKitx403 was designed around strong security primitives that eliminate entire classes of vulnerabilities common in traditional auth.
1. Ed25519-Solana Signatures
The protocol uses ed25519-solana signatures, matching Solana's wallet cryptography exactly.
2. No Sessions, No Cookies
Authentication is stateless. No server sessions, no JWTs, no refresh tokens. The client signs each protected request.
3. Replay Protection
- Nonce stored in a replay store
- Reused nonce is rejected
- Expired challenges are rejected
- Method/path mismatch is rejected
4. Binding Rules
The server binds each signature to:
- HTTP method
- HTTP path
- (Optional) Origin
- (Optional) User-Agent
5. Canonical JSON + Base64url
Prevents ambiguity attacks and ensures cross-language compatibility.
6. Clock Skew Safety
SDKs enforce timestamp tolerance (default +/- 120 seconds).
7. Payload Integrity
Payloads inside challenges cannot be tampered with due to canonicalization and signature binding.
OpenKitx403 is for authentication, not message-level encryption. Always use HTTPS in production.
Internal Client Logic (TS + Python)
TS: Extract Challenge
private extractChallenge(headers: Headers): Challenge {
const header = headers.get("WWW-Authenticate");
if (!header) throw new Error("Challenge missing");
return decodeChallenge(header);
}
TS: Canonical JSON
const payload = JSON.stringify(challenge, Object.keys(challenge).sort());
Python: Canonical JSON
payload = json.dumps(challenge, sort_keys=True, separators=(",", ":"))
Signing (TS)
const message = new TextEncoder().encode(signingString);
const signature = await this.wallet.signMessage(message);
Signing (Python)
signature = keypair.sign_message(signing_string.encode())
Authorization Header
Authorization: OpenKitx403 addr="{addr}", sig="{sig}", challenge="{b64}", ts="{ts}", nonce="{nonce}", bind="{method}:{path}"
Internal Server Logic (Python)
auth_header = request.headers.get("Authorization")
if not auth_header:
# Issue challenge
header_value, _ = create_challenge(
method=request.method,
path=request.url.path,
audience=self.config.audience,
issuer=self.config.issuer,
ttl_seconds=self.config.ttl_seconds,
ua_binding=self.config.ua_binding,
origin_binding=self.config.origin_binding,
)
# 403 + WWW-Authenticate
...
# Verify authorization
result = await verify_authorization(
auth_header=auth_header,
method=request.method,
path=request.url.path,
config=self.config,
) # checks version, alg, audience, issuer, expiry, nonce, bindings, signature
if not result.ok:
# return 401/403
...
# Attach user
request.state.openkitx403_user = {"address": result.address}
Complete Examples
Browser + FastAPI Full Stack
Frontend (React)
import { useState } from "react";
import { OpenKit403Client } from "@openkitx403/client";
export function App() {
const [client] = useState(() => new OpenKit403Client());
const [address, setAddress] = useState<string | null>(null);
const [profile, setProfile] = useState<any | null>(null);
const login = async () => {
await client.connect("phantom");
const response = await client.authenticate({
resource: "http://localhost:8000/profile",
method: "GET",
});
if (response.ok) {
setAddress(client.getAddress());
const data = await response.json();
setProfile(data);
}
};
return (
<div>
{!address ? (
<button onClick={login}>Connect Wallet</button>
) : (
<div>
<p>Wallet: {address}</p>
<pre>{JSON.stringify(profile, null, 2)}</pre>
</div>
)}
</div>
);
}
Backend (FastAPI)
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from openkitx403 import OpenKit403Middleware, require_openkitx403_user
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(
OpenKit403Middleware,
audience="http://localhost:3000",
issuer="my-api",
ttl_seconds=60,
)
@app.get("/profile")
async def profile(user = Depends(require_openkitx403_user)):
return {
"address": user.address,
"username": f"User_{user.address[:6]}",
"joinedAt": "2025-01-01T00:00:00Z",
}
Python Script → Protected API
from solders.keypair import Keypair
from openkitx403_client import OpenKit403Client
keypair = Keypair()
client = OpenKit403Client(keypair)
res = client.authenticate("https://api.example.com/admin", method="POST", json_data={"force": True})
print(res.json())
API Reference (High-Level)
TypeScript Client
class OpenKit403Client
constructor(walletOrKeypair?: WalletProvider | Keypair)
Initialize the client with a browser wallet or a Solana keypair (Node).
async connect(wallet: WalletProvider | string): Promise<void>
Connect to a Solana wallet (e.g. "phantom", "backpack").
async authenticate(resource: string | AuthRequest, options?: RequestInit): Promise<Response>
Performs complete 403 challenge flow: request → 403 → sign → retry with Authorization → final Response.
getAddress(): string
Returns the currently connected wallet address.
disconnect(): void
Disconnects the current wallet.
Python Server
class OpenKit403Middleware
OpenKit403Middleware(audience: str, issuer: str, ttl_seconds: int = 60, clock_skew_seconds: int = 120, bind_method_path: bool = False, origin_binding: bool = False, ua_binding: bool = False, replay_backend: str = "memory", token_gate: Optional[Callable[[str], bool]] = None, excluded_paths: Optional[list[str]] = None, allowed_origins: Optional[list[str]] = None)
FastAPI middleware that issues challenges and verifies signatures.
Python Client
class OpenKit403Client
__init__(self, keypair: Keypair)
Initialize client with a Solana keypair.
authenticate(url: str, method: str = "GET", headers: dict | None = None, data: dict | None = None, json_data: dict | None = None) -> requests.Response
Executes the full challenge → sign → retry loop.
Python Agent SDK
class OpenKit403Agent
__init__(self, keypair: Keypair, options: AgentAuthOptions | None = None)
async execute(self, options: AgentExecuteOptions) -> AgentAuthResult