Expand description
§solid-pod-rs-idp
Solid-OIDC identity provider for
solid-pod-rs –
authorization-code flow, DPoP-bound tokens, JWKS publication,
dynamic client registration, and credentials login.
§Feature flags
| Flag | Purpose |
|---|---|
axum-binder | Ready-made axum Router that wires all IdP endpoints. |
passkey | WebAuthn/passkey authentication via webauthn-rs. |
schnorr-sso | NIP-07 Schnorr SSO (Nostr key login). |
§Modules
provider—Providerorchestrator:/auth,/token,/meendpoints.discovery— OIDC discovery document builder (/.well-known/openid-configuration).jwks— JWKS key management and/.well-known/jwks.jsonpublication.credentials— Email + password login flow with rate limiting.registration— Dynamic Client Registration and Client Identifier Documents.tokens— DPoP-bound access-token issuance.session— Opaque-token session store.user_store— PluggableUserStoretrait withInMemoryUserStorefor tests.invites— Invite-token minting, storage, and validation.error—ProviderErrorwith RFC 6749 error codes.- [
passkey] — (featurepasskey) WebAuthn registration and authentication. - [
schnorr] — (featureschnorr-sso) NIP-07 Schnorr challenge/response. - [
axum_binder] — (featureaxum-binder) Pre-built axum router.
§Quick start
use solid_pod_rs_idp::{Provider, ProviderConfig, Jwks, SessionStore,
registration::ClientStore, user_store::InMemoryUserStore};
use std::sync::Arc;
let user_store = Arc::new(InMemoryUserStore::new());
let jwks = Jwks::generate_es256().unwrap();
let provider = Provider::new(
ProviderConfig::new("https://pod.example/"),
ClientStore::new(), SessionStore::new(), user_store, jwks,
);
let _disco = provider.discovery_document();§Design boundaries
- This crate owns protocol logic only. Transport framing is the
consumer’s job: plug
Providerinto your own router, or enable theaxum-binderfeature for a ready-madeRouter. - Storage is pluggable via
UserStore. The built-inInMemoryUserStoreexists for tests and single-user development; production deployments should ship a persistent store. - DPoP verification delegates to
solid_pod_rs::oidc::verify_dpop_proof. - SSRF protection on Client Identifier Document fetches delegates to
solid_pod_rs::security::is_safe_url. - Rate-limiting uses the core
RateLimitertrait.
§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 requestflowchart 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 std::sync::Arc;
use solid_pod_rs_idp::{
Provider, ProviderConfig, Jwks, SessionStore,
registration::ClientStore,
user_store::{InMemoryUserStore, UserStore},
};
// 1. Seed stores.
let user_store: Arc<dyn UserStore> = Arc::new(InMemoryUserStore::new());
let client_store = ClientStore::new();
let session_store = SessionStore::new();
let jwks = Jwks::generate_es256().unwrap();
// 2. Build the provider.
let provider = Provider::new(
ProviderConfig::new("https://pod.example/"),
client_store,
session_store,
user_store,
jwks,
);
// 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:
[dependencies]
solid-pod-rs-idp = { version = "0.4", features = ["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.
Re-exports§
pub use credentials::login;pub use credentials::validate_password_length;pub use credentials::CredentialsResponse;pub use credentials::LoginError;pub use credentials::MIN_PASSWORD_LENGTH;pub use discovery::build_discovery;pub use discovery::DiscoveryDocument;pub use error::ProviderError;pub use invites::mint_token as mint_invite_token;pub use invites::parse_duration as parse_invite_duration;pub use invites::InMemoryInviteStore;pub use invites::Invite;pub use invites::InviteStore;pub use invites::InviteStoreError;pub use jwks::Jwks;pub use jwks::JwksError;pub use jwks::SigningKey;pub use provider::AuthorizeRequest;pub use provider::AuthorizeResponse;pub use provider::Provider;pub use provider::ProviderConfig;pub use provider::TokenRequest;pub use provider::TokenResponse;pub use provider::UserInfo;pub use registration::register_client;pub use registration::ClientDocument;pub use registration::ClientStore;pub use registration::RegError;pub use registration::RegistrationRequest;pub use session::SessionError;pub use session::SessionId;pub use session::SessionStore;pub use tokens::issue_access_token;pub use tokens::AccessToken;pub use tokens::TokenError;pub use user_store::InMemoryUserStore;pub use user_store::User;pub use user_store::UserStore;pub use user_store::UserStoreError;
Modules§
- credentials
/idp/credentials— email+password login flow (row 79).- discovery
- OIDC discovery document (row 76).
- error
- Crate-wide error type for the IdP.
- invites
- Sprint-11 row 163 — invite-token storage.
- jwks
- JWKS publication + key rotation (row 77, 130).
- provider
- OIDC provider surface:
/auth+/token+/me(rows 74, 76, 77). - registration
- Dynamic Client Registration + Client Identifier Documents (rows 75, 78).
- session
- Opaque-token session store.
- tokens
- Access token issuance.
- user_
store - Pluggable user-storage trait.