Architecture¶
Component Overview¶
graph TB
subgraph "pico-server-auth"
AC[AuthController<br/>/auth/*]
TI[TokenIssuer<br/>RS256 JWT signing]
WV[WalletVerifier<br/>ML-DSA-65, Ed25519, secp256k1]
CS[ChallengeStore<br/>Nonce storage + TTL]
SAS[ServerAuthSettings<br/>Configuration]
end
AC --> TI
AC --> WV
AC --> CS
TI --> SAS
WV --> SAS
CS --> SAS
Client([Client / Wallet]) -->|POST /api/v1/auth/challenge| AC
Client -->|POST /api/v1/auth/sign-in| AC
Client -->|POST /api/v1/auth/login| AC
subgraph "pico-client-auth"
TV[TokenValidator]
JC[JWKSClient]
end
JC -->|GET /api/v1/auth/jwks| AC
TV --> JC How It Fits with pico-client-auth¶
pico-server-auth issues tokens. pico-client-auth validates them. They connect via the JWKS endpoint.
Embedded mode (same process):
graph LR
subgraph "pico-boot Application"
SA[pico-server-auth<br/>Issues JWTs]
CA[pico-client-auth<br/>Validates JWTs]
APP[Your Controllers]
end
SA -->|JWKS via /api/v1/auth/jwks| CA
CA -->|SecurityContext| APP Standalone mode (separate services):
graph LR
subgraph "Auth Service"
SA[pico-server-auth]
end
subgraph "API Service"
CA[pico-client-auth]
APP[Your Controllers]
end
SA -->|JWKS via HTTPS| CA
CA -->|SecurityContext| APP Wallet Challenge/Verify Flow¶
sequenceDiagram
participant W as Wallet Client
participant A as AuthController
participant CS as ChallengeStore
participant WV as WalletVerifier
participant TI as TokenIssuer
W->>A: POST /api/v1/auth/challenge {"address": "0x..."}
A->>CS: create(address) -> nonce
A-->>W: {"challenge": "<nonce>", "ttl": 60}
Note over W: Sign nonce with private key
W->>A: POST /api/v1/auth/sign-in {"address", "public_key", "signature", "challenge", "algorithm"}
A->>CS: validate(address, nonce)
CS-->>A: true (nonce consumed)
A->>WV: verify(algorithm, public_key, message, signature)
WV-->>A: true
A->>TI: issue_access_token(subject=address, role="wallet")
A->>TI: issue_refresh_token(subject=address)
TI-->>A: access_token, refresh_token
A-->>W: {"access_token", "refresh_token", "address", "algorithm"} Token Issuance¶
TokenIssuer generates a fresh RSA-2048 keypair on startup (key ID: pico-server-auth-1). Access tokens include:
| Claim | Source |
|---|---|
sub | Wallet address or email |
iss | ServerAuthSettings.issuer |
aud | ServerAuthSettings.audience |
iat | Current timestamp |
exp | iat + access_token_expire_minutes * 60 |
role | "wallet" or "admin" |
algorithm | Wallet algorithm (wallet login only) |
wallet_address | Wallet address (wallet login only) |
Refresh tokens include sub, iss, aud, iat, exp, and "type": "refresh".
Challenge Store¶
The ChallengeStore protocol defines three methods:
create(address) -> str— Generate and store a nonce for the given address.validate(address, nonce) -> bool— Consume and verify a nonce (one-time use).cleanup() -> int— Remove expired entries, return count removed.
The default InMemoryChallengeStore stores challenges in a dict with timestamps. Each create() call triggers cleanup() to prune expired entries. Challenges are single-use: validate() removes the entry on lookup.