solid-pod-rs-idp
Status: 0.4.0-alpha.2 — Sprint 10–12 Solid-OIDC provider.
Rust port of the JSS identity provider (JavaScriptSolidServer/src/idp/*).
This crate owns the protocol surface; transport framing is the
consumer's decision (enable axum-binder for a ready-made Router,
or plug Provider into any router you like).
What landed in Sprint 10
Parity rows flipped from missing → present (tracked in
../../docs/PARITY-CHECKLIST.md):
| Row | Endpoint / feature | JSS ref |
|---|---|---|
| 74 | /idp/auth — authorization-code flow |
src/idp/provider.js:307-317 |
| 75 | /idp/reg — Dynamic Client Registration |
src/idp/provider.js:147-156 |
| 76 | /.well-known/openid-configuration |
src/idp/index.js:203-237 |
| 77 | /.well-known/jwks.json |
src/idp/index.js:240-244 |
| 78 | Client Identifier Documents (SSRF-guarded) | src/idp/provider.js:22-85 |
| 79 | /idp/credentials (email+password + rate-limit) |
src/idp/credentials.js |
| 130 | JWKS publication (IdP side) | src/idp/keys.js |
WebAuthn + Schnorr SSO (rows 80, 81)
Sprint 11 lands real backends for both rows:
| Row | Backend | Feature flag | Notes |
|---|---|---|---|
| 80 | [WebauthnPasskey] on top of webauthn-rs 0.5 |
passkey |
Reasonable defaults: user-verification required, EdDSA+ES256, single-step registration, in-memory challenge/credential store. Swap for a persistent store via a custom [PasskeyBackend] impl. |
| 81 | [Nip07SchnorrSso] on top of core nip98-schnorr |
schnorr-sso |
32-byte CSPRNG challenges, 5-minute default TTL, one-shot consume-on-verify. Canonical digest is SHA-256(token ‖ user_id ‖ pubkey). |
The trait types ([PasskeyBackend], [SchnorrSso]) stay stable so
integrators who want to bring their own backend — e.g. attestation-
pinned WebAuthn, or Redis-backed Schnorr state — can swap the default
impl without touching Provider.
The zero-op PasskeyTodo and SchnorrTodo types remain as
#[doc(hidden)] fallbacks: useful for wiring a provider up in tests
before deciding which backend to enable.
What is wontfix-in-crate
| Row | Why |
|---|---|
| 82 | HTML interaction pages (login / consent / register). JSS bundles Handlebars templates in src/idp/views.js. We do not ship a view layer because the right choice depends on the consumer's existing stack (Askama, Leptos, Tera, Yew, or plain format!). A minimal Askama adapter on top of this crate is < 300 LOC and should live in a host-app crate where the operator controls the HTML. |
Authorization code flow
sequenceDiagram
participant App as Client App
participant IDP as IdP Provider
participant US as UserStore
participant SS as SessionStore
participant JWKS as JWKS (ES256)
Note over App: Discovery
App->>IDP: GET /.well-known/openid-configuration
IDP-->>App: issuer, endpoints, DPoP algs, PKCE
Note over App: Dynamic Client Registration
App->>IDP: POST /idp/reg {redirect_uris}
IDP-->>App: {client_id, client_secret}
Note over App: Authorization
App->>IDP: GET /idp/auth?code_challenge=S256(verifier)
IDP->>US: find_by_email + verify_password
Note over IDP: Password ≥ 8 chars (CWE-521)
US-->>IDP: User {webid, id}
IDP->>SS: create_session + issue auth_code
IDP-->>App: 302 → redirect_uri?code=…
Note over App: Token Exchange
App->>IDP: POST /idp/token {code, code_verifier, DPoP proof}
IDP->>SS: consume auth_code (single-use)
IDP->>JWKS: sign access token (ES256)
Note over IDP: cnf.jkt = DPoP thumbprint
Note over IDP: ath = SHA-256(access_token)
IDP-->>App: {access_token, token_type: "DPoP"}
Note over App: Resource Access
App->>App: Attach DPoP proof per request
flowchart LR
subgraph cred ["Credentials endpoint (/idp/credentials)"]
direction TB
RL["Rate limiter<br/>10/min per IP"] --> VAL["Validate email +<br/>password (≥ 8 chars)"]
VAL --> AUTH["UserStore lookup<br/>+ argon2id verify"]
AUTH --> TOK["Issue access token<br/>ES256-signed JWT"]
TOK --> BIND{"DPoP proof<br/>supplied?"}
BIND -->|yes| DPOP["token_type: DPoP<br/>cnf.jkt bound"]
BIND -->|no| BEARER["token_type: Bearer"]
end
style RL fill:#e74c3c,stroke:#c0392b,color:#fff
style VAL fill:#f39c12,stroke:#d68910,color:#fff
style AUTH fill:#9b59b6,stroke:#7d3c98,color:#fff
style TOK fill:#2ecc71,stroke:#1a9850,color:#fff
Minimum-viable flow
use Arc;
use ;
// 1. Seed stores.
let user_store: = new;
let client_store = new;
let session_store = new;
let jwks = generate_es256.unwrap;
// 2. Build the provider.
let provider = new;
// 3. Serve discovery + JWKS directly from the provider:
let _discovery = provider.discovery_document;
let _jwks_doc = provider.jwks.public_document;
Axum binder
Enable axum-binder to get a Router with discovery, JWKS,
registration, and credentials pre-wired:
[]
= { = "0.4", = ["axum-binder"] }
/idp/auth and /idp/token are NOT on the binder — their request
shape (session cookies, form-encoded bodies, 302 redirects) is too
app-specific for a generic binder. Wire them against your own
framework session middleware.
Design deviations from JSS
Honest list of shape differences (not behaviour differences — those should be zero):
- Signing algorithm. JSS publishes both RS256 and ES256; we
publish ES256 only in Sprint 10 (Solid-OIDC mandates ES256 for
DPoP, every Solid RP we checked accepts ES256 id-tokens, and it
skips pulling
rsainto our dep graph). Additional algs can be inserted viaJwks::insert_signing_key. - Password hash. JSS uses
bcrypt(src/idp/accounts.js); we useargon2id(stronger, OWASP-preferred). Re-hashing on next successful login is the consumer's migration story. - Session storage. JSS persists sessions to disk via
oidc-provider's filesystem adapter. We ship an in-memory store with a pluggable trait; disk persistence is the consumer's choice (serialise theSigningKey::private_pemand session records to their own backend). - Code format. JSS generates opaque client ids as
client_<base36-timestamp>_<random>. We mirror the format. - View layer. JSS bundles Handlebars templates; we don't (see row 82 above).
Sprint 12 changes
- Password-length validation (CWE-521).
MIN_PASSWORD_LENGTH = 8constant andvalidate_password_length()helper mirror JSS commit1feead2.LoginError::PasswordTooShortvariant returns HTTP 400 via the Axum binder.InMemoryUserStore::insert_userenforces the same minimum at registration time. - Re-exported:
validate_password_length,MIN_PASSWORD_LENGTHfrom crate root.
Tests
91 unit tests cover:
- Discovery document shape (
webidin scopes,noneauth method, DPoP algs, PKCE S256, issuer trailing-slash normalisation). - JWKS publication, key rotation with retention window, prune-expired, round-trip through PKCS8 PEM.
- Opaque dynamic client registration + Client Identifier Documents (fetch, cache, id-mismatch rejection, SSRF guard trips on private IP, missing-redirect-uris rejection).
- Session create/lookup/revoke + authorisation-code single-use + TTL expiry.
/idp/credentialsemail+password: correct password, wrong password, unknown user, blank input, DPoP-bound vs Bearer, rate limit tripping at 11th attempt.- Authorisation-code flow end-to-end: issue code → redeem at
/token→ verify DPoP-bound access token. Plus negative cases (missing DPoP, wrong htu, PKCE mismatch, unregistered redirect, no PKCE attempt). - Access-token issuance with DPoP
cnf.jktbinding; Bearer issuance when no DPoP thumbprint is passed;ath_hashknown-value check. - Trait hook callability (passkey / schnorr null backends return
Unimplemented). - Password-length validation: too-short (7 chars), exactly 8, longer,
empty;
MIN_PASSWORD_LENGTHconstant value. - Registration rejects short passwords at
insert_usertime.
Licence
AGPL-3.0-only.