Skip to main content

Crate solid_pod_rs_idp

Crate solid_pod_rs_idp 

Source
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

FlagPurpose
axum-binderReady-made axum Router that wires all IdP endpoints.
passkeyWebAuthn/passkey authentication via webauthn-rs.
schnorr-ssoNIP-07 Schnorr SSO (Nostr key login).

§Modules

  • providerProvider orchestrator: /auth, /token, /me endpoints.
  • discovery — OIDC discovery document builder (/.well-known/openid-configuration).
  • jwks — JWKS key management and /.well-known/jwks.json publication.
  • 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 — Pluggable UserStore trait with InMemoryUserStore for tests.
  • invites — Invite-token minting, storage, and validation.
  • errorProviderError with RFC 6749 error codes.
  • [passkey] — (feature passkey) WebAuthn registration and authentication.
  • [schnorr] — (feature schnorr-sso) NIP-07 Schnorr challenge/response.
  • [axum_binder] — (feature axum-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 Provider into your own router, or enable the axum-binder feature for a ready-made Router.
  • Storage is pluggable via UserStore. The built-in InMemoryUserStore exists 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 RateLimiter trait.

§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 missingpresent (tracked in ../../docs/PARITY-CHECKLIST.md):

RowEndpoint / featureJSS ref
74/idp/auth — authorization-code flowsrc/idp/provider.js:307-317
75/idp/reg — Dynamic Client Registrationsrc/idp/provider.js:147-156
76/.well-known/openid-configurationsrc/idp/index.js:203-237
77/.well-known/jwks.jsonsrc/idp/index.js:240-244
78Client Identifier Documents (SSRF-guarded)src/idp/provider.js:22-85
79/idp/credentials (email+password + rate-limit)src/idp/credentials.js
130JWKS publication (IdP side)src/idp/keys.js

§WebAuthn + Schnorr SSO (rows 80, 81)

Sprint 11 lands real backends for both rows:

RowBackendFeature flagNotes
80[WebauthnPasskey] on top of webauthn-rs 0.5passkeyReasonable 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-schnorrschnorr-sso32-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

RowWhy
82HTML 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 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):

  1. 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 rsa into our dep graph). Additional algs can be inserted via Jwks::insert_signing_key.
  2. Password hash. JSS uses bcrypt (src/idp/accounts.js); we use argon2id (stronger, OWASP-preferred). Re-hashing on next successful login is the consumer’s migration story.
  3. 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 the SigningKey::private_pem and session records to their own backend).
  4. Code format. JSS generates opaque client ids as client_<base36-timestamp>_<random>. We mirror the format.
  5. View layer. JSS bundles Handlebars templates; we don’t (see row 82 above).

§Sprint 12 changes

  • Password-length validation (CWE-521). MIN_PASSWORD_LENGTH = 8 constant and validate_password_length() helper mirror JSS commit 1feead2. LoginError::PasswordTooShort variant returns HTTP 400 via the Axum binder. InMemoryUserStore::insert_user enforces the same minimum at registration time.
  • Re-exported: validate_password_length, MIN_PASSWORD_LENGTH from crate root.

§Tests

91 unit tests cover:

  • Discovery document shape (webid in scopes, none auth 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/credentials email+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.jkt binding; Bearer issuance when no DPoP thumbprint is passed; ath_hash known-value check.
  • Trait hook callability (passkey / schnorr null backends return Unimplemented).
  • Password-length validation: too-short (7 chars), exactly 8, longer, empty; MIN_PASSWORD_LENGTH constant value.
  • Registration rejects short passwords at insert_user time.

§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.