48. Signed Local OIDC Testing
Important
Dependency examples pin the v13 prerelease
=13.0.0-alpha.4exactly: Cargo never selects a prerelease on its own, and the v12 line reached end of life on 16 October 2026 (see the release record). Commit Cargo.lock for reproducible builds. Use path dependencies only when intentionally testing checkout-local changes.
Rullst Connect includes an explicitly mounted local identity-provider fixture so
an application can exercise a cryptographically verified OIDC flow without a
third-party account. Unlike an in-process provider stub, this path traverses
HTTP discovery, authorization, token exchange, JWKS retrieval and ID-token
verification through the ordinary OidcProvider implementation.
Enable and bind the fixture
Enable the Axum feature in development:
[dev-dependencies]
rullst-connect = { version = "=13.0.0-alpha.4", features = ["axum"] }
A project that enables the rullst facade’s oauth feature (as
cargo rullst make:oauth does) already has this feature: the module is
rullst::connect::mock_idp.
Mount the router only on an exact loopback listener. Axum is reached through
the rullst::web::axum re-export, because generated projects do not depend on
Axum directly:
#![allow(unused)]
fn main() {
use rullst_connect::mock_idp::{MockIdpConfig, mock_router_with_config};
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let issuer = "http://127.0.0.1:8080";
let callback = "http://127.0.0.1:3000/auth/callback";
let config = MockIdpConfig::try_new(
issuer,
"academy-local-client",
"academy-local-secret",
callback,
)?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
rullst::web::axum::serve(listener, mock_router_with_config(config)).await?;
Ok(())
}
}
MockIdpConfig rejects non-HTTP or non-loopback issuers and callbacks, issuer
paths, empty/oversized identifiers and control characters. The application
must still bind the actual listener to loopback; possession of a router does not
control how the host serves it.
Configure the ordinary OIDC client
Use non-placeholder local credentials so OidcProvider traverses the HTTP
fixture: empty or mock_* credentials select the separate network-free
credential fallback instead (and are rejected without the mock feature):
#![allow(unused)]
fn main() {
use rullst_connect::providers::OidcProvider;
async fn provider() -> Result<OidcProvider, rullst_connect::ConnectError> {
OidcProvider::discover(
"http://127.0.0.1:8080",
"academy-local-client",
"academy-local-secret",
"http://127.0.0.1:3000/auth/callback",
).await
}
}
Compose this provider with the server-bound session ceremony from
Tutorial 42. That ceremony generates and
stores state, PKCE verifier and OIDC nonce; the fixture binds the challenge and
nonce to its one-shot authorization grant. The callback then supplies the
consumed values to OidcProvider::get_user.
What the test proves
The checked-in loopback regression proves that the current client can:
- validate discovery metadata on the exact issuer origin;
- preserve an exact registered client and callback;
- exchange one expiring authorization code only once;
- reject a missing, malformed or mismatched S256 PKCE verifier;
- bind the requested nonce into a signed ID token;
- select the Ed25519 public key by
kidfrom JWKS; - validate EdDSA signature, issuer, audience, expiry and nonce; and
- accept only an issued, unexpired bearer token at userinfo.
Authorization grants and access-token digests are process-local and capped at 64 records. A restart erases them. This is intentional test behavior, not a durable identity service.
Non-production boundary
The signing seed, client credentials and identity are deterministic public fixtures. The router has no interactive login, consent, refresh-token, device, federation, account-management, key-rotation or administrative lifecycle. It has not passed an OIDC conformance suite. Never expose it publicly, reuse its key or credentials, or use a successful fixture test as evidence that a live provider/deployment is correctly configured.