Manual contentsSecurityBrowse 113 chapters
Manual 9 min read

OAuth, Apple, and magic-link login

Suprnova exposes OAuth, Sign in with Apple, and passwordless magic links through the framework-owned Auth facade. Magnetar supplies the credential, ceremony, identity, factor-gate, and session engines behind that facade.

The public entry points are:

  • Auth::oauth(provider) for OAuth and Apple.
  • Auth::magic_link() for passwordless email login.

Suprnova does not install routes for these flows. Applications provide small start and callback handlers and decide how to deliver magic-link email.

Initialize Magnetar with OAuth

Configure OAuth on the same MagnetarConfig that initializes password, passkey, session, lockout, and two-factor services. The provider registry is published atomically with those services: if any service cannot be built, none of them becomes visible.

use std::sync::Arc;

use suprnova::{
    AbuseLimiter, App, AutoLinkPolicy, DB, DatabaseConnection, EndpointOverrides,
    FrameworkAbuseLimiter, GoogleOAuthProvider, GoogleProviderConfig, MagnetarConfig,
    MagnetarOAuthHostConfig, MagnetarOAuthProviderConfig, OAuthAuthorizationConfig,
    OAuthHttpTransport, PasskeyConfig, RateLimiterDriver, ReqwestOAuthTransport,
    RevocationTransport, SecretString, init_magnetar,
};

fn auth_config(
    database: DatabaseConnection,
    transport: Arc<dyn OAuthHttpTransport>,
    revocation: Arc<dyn RevocationTransport>,
    limiter: Arc<dyn AbuseLimiter>,
) -> MagnetarConfig {
    let provider = Arc::new(GoogleOAuthProvider::new(
        GoogleProviderConfig {
            client_id: "google-client".to_owned(),
            client_secret: SecretString::from("google-secret".to_owned()),
            redirect_uri: Some("https://app.example.com/auth/google/callback".to_owned()),
            scopes: vec!["openid".to_owned(), "email".to_owned()],
            endpoints: EndpointOverrides::default(),
        },
        revocation,
    ));
    let oauth = MagnetarOAuthHostConfig::new(
        vec![MagnetarOAuthProviderConfig {
            provider,
            redirect_uri: "https://app.example.com/auth/google/callback".to_owned(),
            scopes: vec!["openid".to_owned(), "email".to_owned()],
        }],
        transport,
        limiter,
        OAuthAuthorizationConfig::default(),
        AutoLinkPolicy::default(),
    )
    .expect("valid OAuth host configuration");

    MagnetarConfig::from_sea_orm(database)
        .passkey_config(PasskeyConfig {
            rp_id: "app.example.com".to_owned(),
            rp_origin: "https://app.example.com".to_owned(),
        })
        .oauth(oauth)
}

pub async fn register_auth() -> Result<(), suprnova::FrameworkError> {
    let database = DB::connection()?;
    let transport = Arc::new(ReqwestOAuthTransport::try_default()?);
    let limiter = Arc::new(FrameworkAbuseLimiter::new(
        App::resolve_make::<dyn RateLimiterDriver>()?,
    ));
    init_magnetar(auth_config(
        database.inner().clone(),
        transport.clone(),
        transport,
        limiter,
    ))
    .await
}

The framework re-exports the OAuthProvider contract, the five first-party providers and configuration types, and every type needed to implement a custom provider. ReqwestOAuthTransport supplies production token, userinfo, and revocation I/O. FrameworkAbuseLimiter uses the application's configured RateLimiterDriver. Applications need neither a direct suprnova-magnetar dependency nor hand-written transport and limiter adapters.

MagnetarConfig creates its schema when apply_migrations is enabled, which is the default. Use .apply_migrations(false) only when deployment prepares the same schema separately. A second initialization returns an error instead of replacing any installed engine.

Keep an existing user and session stack

An application can use Magnetar for OAuth ceremonies and provider proof without making Magnetar authoritative for password, passkey, framework-session, or remember-me state. Build the same MagnetarOAuthHostConfig, then install it through the OAuth-only initializer:

use suprnova::{
    MagnetarOAuthOnlyConfig, init_magnetar_oauth_only,
};

let database = DB::connection()?;
init_magnetar_oauth_only(
    MagnetarOAuthOnlyConfig::from_sea_orm(
        database.inner().clone(),
        oauth,
    ),
)
.await?;

Start the ceremony normally with Auth::oauth(provider).begin(). In the callback, call verify_oauth_identity(code, state), map the verified provider subject into the application's own user table, and establish the existing framework session with Auth::login. Do not call complete in this mode: complete applies Magnetar's default account and session mapping, while the purpose of OAuth-only initialization is to leave those decisions with the application.

OAuth-only and full default initialization are alternatives. A second initializer fails instead of mixing session authorities.

GitHub provider requirements

GitHub's REST user endpoint requires a User-Agent; a community provider adds it, and any media-type Accept value it needs, through OAuthProvider::userinfo_headers. Suprnova adds the bearer Authorization header separately and rejects provider attempts to override it.

GitHub's /user response includes an email only when the user made it public. The verified primary address requires a second /user/emails request, while resolve_identity deliberately performs no I/O and receives one userinfo response. A GitHub provider can return email: None and use Suprnova's email completion ceremony, or point userinfo_endpoint at a host adapter that combines /user with the verified primary email. Do not treat an unverified or merely public address as account ownership.

Session binding

OAuth begin requires SessionMiddleware. Magnetar binds the ceremony to a digest of the initiating framework session, so the callback cannot be moved to another browser session.

Successful password, magic-link, passkey, and OAuth sign-in rotates the framework session ID and CSRF token, records the application user ID, and stores an opaque Magnetar web binding. Remember-me hydration rotates both the Magnetar credential and the framework session binding.

Start an OAuth flow

Use begin in the provider's start handler:

use suprnova::Auth;

let kickoff = Auth::oauth("google").begin().await?;
// Return an HTTP redirect to kickoff.authorization_url.

The returned OAuthKickoff contains:

  • authorization_url, the URL to send to the browser.
  • state, the single-use selector bound to the initiating session.

Magnetar owns state generation, PKCE policy, ceremony persistence, provider exchange, identity verification, and abuse limiting. The host controller owns the HTTP redirect and callback route.

Verify or complete the callback

The callback has two entry points:

Method Result Side effects
verify_oauth_identity(code, state) OAuthIdentity Verifies the provider proof and returns the provider, subject, verified email, and display name without creating an application session.
complete(code, state) (User, Session) Resolves the identity through the installed host engine, applies account-link policy and the factor gate, rotates the framework session, and returns the framework-owned user and Magnetar session values.
let identity = Auth::oauth("google")
    .verify_oauth_identity(&code, &state)
    .await?;

let (user, session) = Auth::oauth("google")
    .complete(&code, &state)
    .await?;

OAuthIdentity.email is present only when the provider supplied a verified email. Persist the provider and subject as the stable external identity. Email is not a stable provider identifier.

OAuth completion does not treat possession of an unverified email string as proof that the caller owns an existing application account.

The completion result can require more work instead of issuing a session:

  • Email completion required returns HTTP 409 when the provider identity needs a separate verified-email ceremony.
  • Explicit link required returns HTTP 409 when an existing verified account must authorize the link.
  • Factor required returns HTTP 401 when account policy requires a second factor before session issuance.

A verified-email completion that wins the first-email-proof boundary reclaims an unverified squatted account atomically. The transaction advances the auth epoch, removes provisional credentials, revokes old sessions and remember credentials, and attaches the verified provider account. A verified account is never auto-linked by email alone.

Sign in with Apple

Apple uses the same Auth::oauth("apple") facade, but its callback commonly uses response_mode=form_post. Register the callback as a POST route and pass the optional Apple user form field through the Apple-specific methods:

let identity = Auth::oauth("apple")
    .verify_apple_identity(&code, &state, form_post_user.clone())
    .await?;

let (user, session) = Auth::oauth("apple")
    .complete_with_apple_form_post(&code, &state, form_post_user)
    .await?;

AppleIdentity includes the stable subject, optional verified email, email_verified, and is_private_email. Persist the subject as the stable key. Apple can supply the display name only during the first authorization, so the provider adapter must preserve that first form_post value.

Apple token and identity verification belongs to the installed provider implementation. Current Magnetar providers require signature, issuer, audience, expiry, and nonce checks rather than trusting an ID token's decoded JSON.

Magic-link login uses the installed Magnetar password/session engine. The framework returns the plaintext single-use token, while the application owns mail composition and URL shape:

use suprnova::{Auth, Mail};

let token = Auth::magic_link()
    .send("alice@example.com", "https://app.example.com/auth/magic")
    .await?;

let url = format!("https://app.example.com/auth/magic?token={token}");
Mail::to("alice@example.com")
    .send(MagicLinkMail { url })
    .await?;

let (user, session) = Auth::magic_link().consume(&token).await?;

send applies the authentication abuse budget before token issuance. consume is single-use, applies the factor gate, binds the resulting session into the framework request session, and returns the user and Magnetar session.

For an unverified pre-existing account, successful magic-link consumption is a first email proof. The transaction reclaims the account and removes provisional password, passkey, linked-account, two-factor, session, and remember state so a prior squatter cannot retain access.

Routes to add

A typical application adds these routes:

get!("/auth/oauth/{provider}/start", controllers::oauth::start),
get!("/auth/oauth/{provider}/callback", controllers::oauth::callback),
post!("/auth/apple/callback", controllers::oauth::apple_callback),
post!("/auth/magic", controllers::magic_link::send),
get!("/auth/magic/callback", controllers::magic_link::consume),

Apply SessionMiddleware to every OAuth and passkey start/callback route. The session carries the ceremony selector and binds the round trip to the browser that started it.

Authentication migration

The suprnova-magnetar crate includes a shape-aware migration engine for Torii, Suprnova web, Suprnova API, and existing Magnetar schemas. It is a library surface and example, not a suprnova CLI subcommand.

Enable the migration feature plus the source database driver and run a dry plan before applying. For PostgreSQL:

cargo run -p suprnova-magnetar \
  --features migration,seaorm-postgres \
  --example migrate -- \
  --source-shape torii \
  --database-url "$SOURCE_DATABASE_URL" \
  --app-database-url "$DATABASE_URL"

Use seaorm-mysql or seaorm-sqlite instead when that is the source and application database driver.

Add --apply to apply the reviewed plan. The runner rechecks source and schema fingerprints before import, records retry state, refuses identity collisions, and uses transactional imports. MySQL same-database migrations use a write-barrier-protected shadow swap with resumable restore and abort paths.

Keep the generated plan and report in deployment records. Do not apply a plan whose source fingerprint changed after review.

Reference

  • Default boot: MagnetarConfig, PasskeyConfig, and init_magnetar.
  • Facades: Auth::oauth(provider) and Auth::magic_link().
  • OAuth installation: MagnetarConfig::oauth, ReqwestOAuthTransport, and FrameworkAbuseLimiter.
  • Migration library: magnetar::migration from the suprnova-magnetar crate.
  • Bearer authentication: BearerTokenMiddleware.

Next

  • Authentication covers password, passkey, guards, framework sessions, and engine initialization.
  • Auth flows covers email verification, password reset, lockout, and two-factor authentication.
  • Mail covers application-owned magic-link delivery.
  • Session covers the browser session that binds OAuth and passkey ceremonies.