Suprnova ships three torii-backed login methods behind the Auth facade:
generic OAuth (GitHub, Google, or any OIDC/OAuth2 provider), Sign in with
Apple, and passwordless magic links. They share one prerequisite
(init_torii plus the ceremony migration) and the same facade shape -
Auth::oauth(provider) / Auth::magic_link() - and none of them ship routes:
you add a thin controller (start + callback) and the framework does the CSRF
state, PKCE, token exchange, identity verification, user upsert, and session
minting.
The whole surface lives in framework/src/torii_integration/. There is no
framework env-var contract for any of it - every credential is passed
programmatically (pull your own from the environment); this chapter's examples
use std::env::var(...) purely to show where your secrets go.
Prerequisites
-
Initialise torii once at boot - this backs the user upsert and session creation:
use ; // in bootstrap::register(), after DB::init() init_torii.await?; -
Run the ceremony migration. OAuth and Apple stash a short-lived (10-minute) CSRF-
state+ PKCE ceremony in theauth_ceremony_tokenstable. Register the migrationm20251209_000000_create_auth_ceremony_tokens_tablein yourMigrator(the starter kits already include it). Optionally schedulesuprnova::torii_integration::ceremony::prune_expired()to GC stale rows. -
SessionMiddlewareon the OAuth start route.begin()writes thestateinto the session; a sessionless call fails with a 500.
Magic links need only step 1.
Generic OAuth (GitHub, Google, custom)
Configure a provider
Register each provider once at startup. The registry is process-global and idempotent, so re-registering the same provider just replaces the config:
use Auth;
use OAuthProviderConfig;
oauth.configure;
The well-known authorize/token/userinfo endpoints are built in for github,
google, and apple. For any other provider - or a self-hosted / test server -
supply them yourself:
use EndpointOverrides;
oauth.configure;
Start the flow (authorize URL)
// GET /auth/oauth/github/start (route MUST carry SessionMiddleware)
let kickoff = oauth.begin.await?;
// kickoff.authorization_url - redirect the browser here
// kickoff.state - CSRF state, already stored in the session for you
begin() mints the CSRF state (UUID v4) and an RFC 7636 PKCE
verifier/S256 challenge, records the ceremony (10-minute TTL), and returns the
provider authorize URL. Redirect the user to authorization_url.
Complete the flow - verify vs complete
On the callback you have two entry points (split in 0.5.4). Pick by whether your
users table is torii's schema:
| Method | Returns | Side effects | Use when |
|---|---|---|---|
verify_oauth_identity(code, state) |
OAuthIdentity { provider, subject, email, name } |
None - verifies the ceremony, exchanges the code, fetches userinfo, extracts a verified email + stable subject. No user, no session. |
Your app owns its users table and you want to look up / create the user yourself. |
complete(code, state) |
(User, Session) |
Upserts the user into torii (get_or_create_user) and mints a session. |
Your users table is torii's schema. |
// Custom users table:
let id = oauth.verify_oauth_identity.await?;
// id.subject is the stable provider id; id.email is verified-or-None.
let user = upsert.await?;
// …or, torii-backed:
let = oauth.complete.await?;
A verify-returned email is always a verified address (OIDC email_verified,
GitHub treated as verified, or the /emails fallback); an unverified or absent
email comes back as None and repeat logins resolve by subject.
Routes you add
The framework provides no OAuth routes - wire two thin handlers (mirror the shape
of the existing auth_verify / auth_reset controllers in the starter kit):
// start - redirects to the provider
get!,
// callback - GitHub/Google use GET ?code&state
get!,
Put the /start route (at least) behind SessionMiddleware.
Sign in with Apple
Apple is the same facade - Auth::oauth("apple") - with a few Apple-specific
rules baked in:
- The callback is a
POST. Apple usesresponse_mode=form_post, so the redirect deliverscode+statein a form body, not query params. Register the Apple callback as apost!route and read the fields from the form. - No PKCE. Apple rejects
code_challenge, so the authorize URL omits it (the client secret is a signed JWT instead). client_secretis unused - leave itString::new(). Suprnova mints the short-lived JWT client secret from your.p8key on each token exchange.- ID tokens are verified against Apple's JWKS (RS256) since 0.5.6, not trusted structurally.
Supply your Apple key - AppleKeyPair
AppleKeyPair is the one Apple type re-exported for apps (so you need no direct
apple dependency). Build it from your .p8 signing key:
use AppleKeyPair;
let key = from_file?;
// or: AppleKeyPair::from_base64(key_id, b64) / from_pem_bytes(key_id, bytes)
Configure Apple
use OAuthProviderConfig;
oauth.configure;
Complete the Apple flow
Same split as generic OAuth. complete upserts + sessions; the verify path
returns an AppleIdentity for a custom users table:
// POST /auth/apple/callback - read code + state from the FORM body
let = oauth.complete.await?;
// …or custom users table:
let id = oauth.verify_apple_identity.await?;
// id: AppleIdentity { provider, subject, email, email_verified, is_private_email }
AppleIdentity.email is Some(_) only when Apple asserts it verified; an
unverified email is refused (401) before the identity is built. is_private_email
is set when the user chose Apple's private-relay address - persist the subject
as the stable key, since the relay address is the only email you'll get.
Magic-Link Login
Passwordless email login, torii-backed, via Auth::magic_link(). The framework
issues and verifies the token; you email the link (it never sends mail
itself), which composes cleanly with the Mail chapter.
use Auth;
// POST /auth/magic - request a link
let token = magic_link
.send
.await?;
// Build the link and email it yourself:
to
.send
.await?;
// GET /auth/magic?token=… - consume it (single-use; a second call fails)
let = magic_link.consume.await?;
The user is auto-created on first use. send returns the plaintext token so
you control the URL shape and delivery.
Note -
TokenPurpose::MagicLink. Theauth_flowsTokenPurposeenum has aMagicLinkvariant (added in 0.5.5), but it is a reserved discriminator for the genericTokenStore- no built-in flow consumes it. The working, supported magic-link path isAuth::magic_link()above. Only reach forTokenPurpose::MagicLinkif you are hand-rolling your own flow on theauth_flow_tokenstable.
A note on configuration
None of these methods read framework environment variables - provider IDs,
secrets, redirect URLs, and Apple keys are all passed to configure(...)
programmatically. Load them however you like (std::env::var, a typed config
struct, a secret manager) and register providers once during bootstrap. This
keeps multi-tenant / per-deploy provider setups first-class instead of forcing a
fixed env-var naming scheme.
Reference
- Facade entry points:
Auth::oauth(provider),Auth::magic_link()(suprnova::Auth) - Config:
suprnova::torii_integration::oauth::{OAuthProviderConfig, EndpointOverrides, AppleKeyPair} - OAuth results:
OAuthKickoff { authorization_url, state },OAuthIdentity { provider, subject, email, name },AppleIdentity { provider, subject, email, email_verified, is_private_email } - Bootstrap:
suprnova::{init_torii, ToriiConfig} - Ceremony store:
auth_ceremony_tokenstable +suprnova::torii_integration::ceremony::prune_expired()
Next
- Authentication - guards, providers, and the
Authenticatableuser model these flows create sessions for - Auth Flows - email verification, password reset, and 2FA
- Mail - sending the magic-link email (and the
MAIL_FROM/MAIL_FROM_NAMEsender config) - Sessions - what the returned
Sessionis and how it's persisted
