Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rullst Connect πŸ¦€

Important

This page documents the unreleased v12 source. Use a path dependency from this checkout until the planned 12.0.0-rc.1 is published.

Vision preserved: message brokers, additional queue transports, remote storage, and media work are retained with explicit status and recommendations in the capability ledger.

Crates.io Downloads Documentation Build License

Rullst Connect is an async OAuth2/OIDC client layer with a shared Provider interface and normalized ConnectUser output. Provider-specific protocols, scopes, claims, and application account-linking rules still require explicit review.

πŸ›‘οΈ Security Engineering

Rullst Connect uses layered tests and repository security checks. CI badges report the state of those checks for the referenced commit; they are not an absolute security guarantee.

Security AuditStatusDescription
OpenSSF ScorecardOpenSSF ScorecardCurrent public supply-chain practice score; not a security certification
CodecovFramework library coverageBlocking 90% target for the measured framework-library scope; the complete repository aggregate now also has its own 90% gate
OpenSSF Best PracticesOpenSSF Best PracticesCurrent project badge from the official programme
Release ProvenanceReleaseProvenance attestations for release artifacts; no SLSA level is claimed here
On-demand fuzzingFuzzingManual time-bounded targets; no continuous OSS-Fuzz claim
Property testsProptestScheduled/manual bounded invariant evidence
Miri research matrixMiriManual bounded evidence; the selected pure-Rust client scope is strict, while network/provider execution remains outside Miri
Kani research harnessesKaniManual bounded formal evidence, not proof of the complete protocol surface
CodeQL SASTCodeQLAdvanced semantic code analysis
Cargo DenyCargo DenyBanning unmaintained/vulnerable crates
Cargo AuditCargo AuditContinuous scanning for crate vulnerabilities
Benchmark CIBenchmarkContinuous performance regression testing
Cargo SemVerSemver ChecksStrict SemVer API breakage checks
Cargo MacheteMacheteDetecting unused and bloated dependencies
Spellcheck CISpellcheckAutomated typo detection across docs and code
Mutation TestingMutantsMutation testing for test suite robustness
Secret ScanningTrufflehogAutomated CI prevention of leaked credentials
Unsafe PolicyUnsafeAudits unsafe usage within the workflow’s declared scope
Panic PolicyPanicsGraceful error handling across the framework

✨ Features

  • πŸš€ Async HTTP: Built on Tokio-compatible request paths and reqwest.
  • 🧩 Standardized: All providers return a unified ConnectUser struct.
  • πŸ›‘οΈ Type-Safe: Robust error handling using thiserror (ConnectError).
  • πŸ”Œ Framework adapters: Core provider APIs are framework-independent; optional extractor features cover the integrations declared in the crate.
  • πŸ” Managed callback transaction: The optional Axum/tower-sessions path generates, stores, expires, validates, and consumes state + PKCE + OIDC nonce.
  • πŸ” Encrypted token snapshots: Versioned AES-256-GCM envelopes bind a refresh generation to one trusted provider/account pair and an explicit key rotation ID before application-owned persistence.
  • πŸ” OIDC Security: Strict discovery validation plus isolated JWKS caches with TTL, refresh on unknown kid, and bounded stale-if-error behavior.
  • πŸšͺ Typed remote revocation: Access and refresh tokens are distinct API operations. Google, GitHub, Discord, Apple, Auth0 and Cognito have bounded protocol adapters; unsupported providers fail explicitly and offline credentials remain network-free.
  • 🏒 Explicit Corporate Proxy: First-class HTTP(S) proxy clients, including bounded Basic proxy authentication without credentials in the endpoint URL.
  • πŸ“Ί Device Flow: Native RFC 8628 support for headless CLI and Smart TV auth.
  • πŸ› οΈ Testing: Empty or mock_* credentials select a deterministic offline transport, while mock_idp supplies a loopback-only signed OIDC fixture with one-shot codes, PKCE, nonce, EdDSA ID tokens and JWKS.

πŸ“š Important Documents:

  • CHANGELOG.md: See what’s new.
  • ISSUES: Any issue? Please report.
  • AUDIT.md: Repository audit record; current workflow evidence remains authoritative.

πŸ“¦ Supported Providers

Official support for 11 core providers:

  1. Google
  2. GitHub
  3. Microsoft / Azure AD
  4. Apple (Sign in with Apple)
  5. Auth0
  6. AWS Cognito
  7. Facebook
  8. X (Twitter) (Strict PKCE requirement)
  9. Discord
  10. LinkedIn
  11. OIDC (OpenID Connect Custom Provider)

Remote token revocation is deliberately narrower than login support. Use Provider::revoke_token for an access token and Provider::revoke_refresh_token for a refresh token; Auth0/Cognito accept only the latter through these adapters, while GitHub accepts only the former. A successful provider response does not clear application cookies, sessions, cached identity, or durable token recordsβ€”the host must commit that local logout lifecycle. Provider revocation endpoints are intentionally idempotent in several ecosystems, so HTTP success is protocol acceptance, not proof that the token was valid immediately before the call.

πŸ› οΈ Installation

Inside a checkout of the unreleased v12 workspace, use its path dependency. After the RC is published, install that exact release train instead.

Published RC command:

cargo add rullst-connect@12.0.0-rc.1
cargo add secrecy

For the recommended Axum session transaction, enable axum-session and add a tower-sessions store:

rullst-connect = { path = "../Rullst/rullst-connect", features = ["axum-session"] }
tower-sessions = "0.15"

Or manually add it to your Cargo.toml:

[dependencies]
rullst-connect = { path = "../Rullst/rullst-connect" }
secrecy = "0.10"
tokio = { version = "1.52", features = ["full"] }

πŸš€ Quick Start

1. Initialize the Provider

Choose your provider and pass your credentials and callback URL:

use rullst_connect::prelude::*;

fn main() -> Result<(), ConnectError> {
let github = GithubProvider::try_new(
    "YOUR_CLIENT_ID",
    "YOUR_CLIENT_SECRET".to_string().into(),
    "http://localhost:3000/auth/github/callback",
)?;
let _ = github;
Ok(())
}

Signed local OIDC fixture

Enable axum and mount mock_idp::mock_router_with_config only on an exact loopback listener. MockIdpConfig::try_new also rejects non-loopback issuer and callback URLs. The resulting fixture exercises discovery, an exact registered client/callback, expiring one-shot authorization codes, optional nonce, S256 PKCE, EdDSA ID-token validation through JWKS and bearer-protected userinfo.

Its signing seed and credentials are deterministic public test material. It is not a production identity provider, interactive login/consent UI, refresh-token service, federation implementation or OIDC conformance suite. Follow the local OIDC testing tutorial for a complete loopback setup.

Explicit corporate proxy

Live providers can receive a first-class proxy-aware transport without relying on ambient HTTP_PROXY state:

#![allow(unused)]
fn main() {
use rullst_connect::client::ReqwestClient;
use rullst_connect::prelude::{ConnectError, GithubProvider};
use std::sync::Arc;

fn github_through_corporate_proxy(
    proxy_password: impl Into<String>,
) -> Result<GithubProvider, ConnectError> {
    let proxy = ReqwestClient::try_with_proxy_basic_auth(
        "https://proxy.corp.example:8443",
        "proxy-user",
        proxy_password,
    )?;
    let github = GithubProvider::try_new(
        "YOUR_CLIENT_ID",
        "YOUR_CLIENT_SECRET".to_string().into(),
        "http://localhost:3000/auth/github/callback",
    )?;
    Ok(github.with_http_client(Arc::new(proxy)))
}
}

Proxy URLs are limited to an HTTP(S) scheme and authority, with no embedded credentials, path, query, or fragment. Authenticated non-loopback proxies must use HTTPS. The configured client uses only that explicit proxy; PAC/WPAD, SOCKS, proxy mTLS identity and deployment certification remain outside this bounded transport.

2. Start a Server-Bound Authorization

The recommended Axum path generates state and PKCE, stores their private counterparts in tower-sessions for ten minutes, and returns only the redirect URL:

#![allow(unused)]
fn main() {
use axum::response::Redirect;
use rullst_connect::prelude::{ConnectError, GithubProvider};
use rullst_connect::extractors::begin_oauth_session;
use tower_sessions::Session;

async fn start_github_authorization(
    session: &Session,
    github: &GithubProvider,
) -> Result<Redirect, ConnectError> {
    let authorization = begin_oauth_session(session, github).await?;
    Ok(Redirect::temporary(authorization.url()))
}
}

Use begin_oidc_session instead for Google, Apple, or a custom OIDC provider. It adds and stores an OIDC nonce as well.

3. Consume the Callback and Get the User

AuthSession consumes the challenge before checking its expiry and state. It then supplies the exact PKCE verifier and optional OIDC nonce to the provider:

#![allow(unused)]
fn main() {
use rullst_connect::extractors::AuthSession;
use rullst_connect::prelude::{
    ConnectError, GithubProvider, Provider as _, UniversalProfile,
};

async fn finish_github_callback(
    auth_session: AuthSession,
    github: &GithubProvider,
) -> Result<UniversalProfile, ConnectError> {
    let params = auth_session.exchange_params()?;
    let user = github.get_user(params).await?;
    Ok(user.universal_profile())
}
}

Only one managed challenge is active per browser session. Starting another login deliberately invalidates the earlier tab. The application must configure a durable production session store, Secure/HttpOnly/SameSite cookies, TLS, registered redirect URLs, account-linking policy, and post-login session rotation. See Server-Bound OAuth/OIDC Sessions.

πŸ›‘οΈ Manual State Handling

Non-Axum hosts may use the framework-neutral primitives directly. The host must atomically take the expected value from a short-lived server-side store before comparison; a reusable cookie value is not equivalent.

The following fragment is host pseudocode because the durable one-time store and callback extractor are deliberately application-provided:

use rullst_connect::pkce::generate_oauth_state;

let state = generate_oauth_state();
store_one_time_state(&state).await?; // application-provided durable operation
let url = github.redirect_url_with_state(&state);

let expected = take_one_time_state().await?; // atomically removes it
callback.verify_state(&expected)?;

πŸ”„ Refreshing Tokens

If the provider returned expires_in plus a refresh token, bind the result to a statically dispatched, process-local coordinator when the callback receives it:

#![allow(unused)]
fn main() {
use rullst_connect::{AutoRefreshingSession, ConnectError, ConnectUser};
use rullst_connect::prelude::ExposeSecret as _;

async fn send_token_to_the_authorized_api(_: &str) -> Result<(), ConnectError> {
    Ok(())
}

async fn call_provider_api(
    github: &rullst_connect::providers::GithubProvider,
    user: &ConnectUser,
    token_received_at: u64,
) -> Result<(), ConnectError> {
    let session = AutoRefreshingSession::from_user_at(
        github,
        user,
        token_received_at,
    )?;
    let lease = session.access_token().await?;
    send_token_to_the_authorized_api(lease.access_token().expose_secret()).await?;
    Ok(())
}
}

The default checks 60 seconds before expiration. Refresh calls cannot overlap; callers waiting behind a successful refresh reuse its state. A response can replace the refresh token only after the lifetime and original provider user ID validate. Use access_token_at in deterministic workers/tests. Seal state_snapshot() with EncryptedTokenSnapshot before writing it to a dedicated application store:

#![allow(unused)]
fn main() {
use rullst_connect::{
    EncryptedTokenSnapshot, RefreshableTokenState, TokenSnapshotBinding,
    TokenSnapshotError, TokenSnapshotKey,
};

fn seal_for_storage(
    state: &RefreshableTokenState,
    key_bytes: [u8; 32],
    local_account_id: &str,
) -> Result<EncryptedTokenSnapshot, TokenSnapshotError> {
    let binding = TokenSnapshotBinding::try_new("github", local_account_id)?;
    let key = TokenSnapshotKey::try_new("oauth-primary-2026", key_bytes)?;
    EncryptedTokenSnapshot::seal(state, &key, &binding)
}
}

Persist only snapshot.as_str(). On restart, validate it with try_from_envelope, select the secret-manager key named by key_id(), and call open with the same trusted binding. Keys must be 32 bytes from a secret manager or CSPRNG; human passwords require an application-selected KDF. The envelope authenticates confidentiality, ownership binding and generation but does not provide a database transaction by itself.

Durable shared-local SQLite state

Enable rullst-connect/sqlite (or umbrella rullst/oauth-sqlite) to store the encrypted generations in one local SQLite file:

#![allow(unused)]
fn main() {
use rullst_connect::{
    RefreshableTokenState, SqliteTokenSnapshotStore, TokenSnapshotBinding,
    TokenSnapshotKey, TokenStoreError,
};

async fn persist_initial_generation(
    state: &RefreshableTokenState,
    key: &TokenSnapshotKey,
    account_id: &str,
) -> Result<(), TokenStoreError> {
    let store = SqliteTokenSnapshotStore::connect(
        "sqlite:///var/lib/my-app/oauth-tokens.sqlite",
        50_000,
    )
    .await?;
    let binding = TokenSnapshotBinding::try_new("github", account_id)?;
    store.insert_initial(&binding, state, key).await?;
    store.close().await;
    Ok(())
}
}

After refreshing from generation n, call compare_and_swap(&binding, n, &replacement, &key). BEGIN IMMEDIATE serializes local writers and only generation n + 1 can replace the observed row. The fixed schema persists its configured 1–1,000,000 row ceiling and restart/key metadata; malformed rows, configuration drift, quota exhaustion and stale replacement/deletion fail with typed redacted errors. The account locator is a SHA-256 pseudonymous digest, not an anonymity guarantee; the provider/account binding is also authenticated inside the ciphertext.

This local CAS does not serialize the earlier call to a remote OAuth provider. Applications must still authorize the account, lease that remote operation, reconcile a provider rotation lost to CAS, configure retry/backoff and local logout, keep keys in a secret manager, protect/backup the directory and provide multi-host replication when needed. A provider that does not support refresh continues to return a typed error.

πŸ”’ Manual PKCE Support

Provider adapters expose PKCE (Proof Key for Code Exchange) where supported by the provider protocol. Some providers such as X (Twitter) v2 require it; applications must preserve and validate the verifier/state for the complete authorization transaction. The generic provider boundary accepts both values explicitly:

#![allow(unused)]
fn main() {
use rullst_connect::{
    ConnectError, ConnectUser,
    pkce::{generate_oauth_state, generate_pkce},
    provider::{ExchangeParams, Provider},
};

async fn exchange_with_pkce<P: Provider>(
    provider: &P,
    authorization_code: &str,
) -> Result<ConnectUser, ConnectError> {
    let state = generate_oauth_state();
    let (code_verifier, code_challenge) = generate_pkce();

    // Persist state + verifier in a short-lived server-side record before redirecting.
    let authorization_url =
        provider.redirect_url_with_pkce_and_state(&code_challenge, &state);
    let _ = authorization_url;

    // After atomically consuming and validating that state in the callback:
    provider
        .get_user(ExchangeParams {
            auth_code: authorization_code,
            code_verifier: Some(&code_verifier),
            ..Default::default()
        })
        .await
}
}

πŸ§‘β€πŸ’» Full Example with Axum

You can find a complete working server using the Axum framework in the examples directory. Just run:

cargo run --example axum_server

πŸ“¦ Releasing a New Version

Connect is released only through the repository-wide, topologically ordered release workflow. Do not publish this crate independently from a working tree. Follow the v12 release guide and require all candidate-SHA gates before creating a release tag.

🀝 Contributing

Feel free to open Issues and submit Pull Requests! Want to add a new provider? It’s easy! Just implement the Provider trait.

πŸ“„ License

This project is licensed under the MIT License.