Skip to content

vapor-community/passage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

145 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Passage

Release Swift 6.3+ License Continous integration Code coverage

A comprehensive identity management and authentication framework for Vapor applications built with Swift. Passage provides secure authentication with minimal configuration while remaining highly extensible through protocol-based architecture.

Status: Alpha

Use with caution. The library is functional, but the API is subject to change before the stable release.

Features

  • πŸ” User Registration & Login - Complete authentication flow with secure password hashing
  • 🚦 Rate Limiting - Throttling of failed login attempts per account and per source
  • πŸ“§ Email Authentication - Email-based identifier with verification codes
  • πŸ“± Phone Authentication - Phone number identifier with SMS verification (requires custom implementation of PhoneDelivery service)
  • πŸ‘€ Username & Password - Traditional username/password authentication
  • ✨ Passwordless Magic Links - Email-based passwordless authentication with one-click login
  • 🎫 JWT Access Tokens - Stateless authentication with JWKS support
  • πŸ”„ Refresh Token Rotation - Secure token refresh with family-based revocation
  • πŸ”“ Password Reset Flow - Email and phone-based password recovery
  • 🌐 OAuth Integration - Federated login (Google, GitHub, custom providers)
  • πŸ”‘ Passkeys (WebAuthn) - Phishing-resistant public-key credentials with signup, sign-in, and "add passkey" flows β€” pluggable backend via PasskeyService
  • πŸ”— Account Linking - Link multiple identifiers to a single user account (automatic or manual)
  • πŸ“‹ Web Forms - Built-in Leaf templates for registration, login, and password reset
  • ⚑ Async Queue Support - Optional background job processing via Vapor Queues
  • πŸ”§ Protocol-Based Services - Pluggable storage, email, phone, and OAuth providers
  • πŸͺ Lifecycle Hooks - Async will/did callbacks for custom policy and audit
  • 🎨 Fully Customizable - Configure routes, tokens, templates, and behavior

Standards Compliance

Passage targets NIST SP 800-63B Authenticator Assurance Level 1 (AAL1), and ships an executable compliance suite at Tests/PassageTests/AAL1/ that pins behavior to specific spec clauses. Coverage spans memorized secrets (Β§5.1.1), session management (Β§7.1), reauthentication (Β§4.1.3 / Β§7.2), and online-guessing throttling (Β§5.2.2) β€” every test name cites the clause it asserts, so the suite doubles as a machine-checked compliance ledger.

Getting Started

Installation

Add Passage to your Package.swift:

dependencies: [
    // πŸ›‚ Authentication and user management for Vapor.
    .package(url: "https://github.com/vapor-community/passage.git", from: "0.3.0"),
]

Then add "Passage" to your target dependencies:

.product(name: "Passage", package: "passage"),

Add PassageOnlyForTest only if you want to use the in-memory store for testing:

.product(name: "PassageOnlyForTest", package: "passage"),

Basic Setup

  1. Set a custom working directory in your scheme and point it to your project folder.
  2. Create a JWKS file keypair.jwks and place it in the root of your project.
  3. Configure Passage in your configure.swift:
// enable Leaf templating to use Passage's built-in views
app.views.use(.leaf)

// enable sessions middleware
app.middleware.use(app.sessions.middleware)

// Configure Passage with in-memory store for testing
try await app.passage.configure(
    services: .init(
        store: Passage.OnlyForTest.InMemoryStore(),
        emailDelivery: nil,
        phoneDelivery: nil,
    ),
    configuration: .init(
        origin: URL(string: "http://localhost:8080")!,
        sessions: .init(enabled: true),
        jwt: .init(
            jwks: .file(path: "\(app.directory.workingDirectory)keypair.jwks")
        ),
        views: .init(
            register: .init(
                style: .minimalism,
                theme: .init(
                    colors: .mintDark
                ),
                identifier: .username
            ),
            login: .init(
                style: .minimalism,
                theme: .init(
                    colors: .mintDark
                ),
                identifier: .username
            )
        )
    )
)

Example Usage

In your routes.swift file, protect routes using Passage's authenticators and guards:

app
    .grouped(PassageSessionAuthenticator())
    .grouped(PassageBearerAuthenticator())
    .grouped(PassageGuard())
    .get("protected") { req async throws -> String in
        let user = try req.passage.user
        return "Hello, \(String(describing: user.id))!"
}

This adds two view endpoints at http://localhost:8080/auth/register and http://localhost:8080/auth/login for user registration and login, as well as a protected route at http://localhost:8080/protected that requires authentication.

Customization

Passage is designed for flexibility through:

  • Comprehensive Configuration - Customize routes, token TTLs, JWT settings, verification flows, OAuth providers, and web forms
  • Protocol-Based Services - Implement your own storage, email delivery, phone delivery, or OAuth providers
  • Extensible Forms - Default form types can be replaced with custom implementations via contracts
  • Stylable Default Views - Default Leaf views with different styles and themes
  • Lifecycle Hooks - Inject async pre/post callbacks around authentication flows; throw from will* hooks to gate flows on custom policy

Services to Implement

Passage exposes six service protocols for pluggable backends. Only Store is required; every other service is optional and unlocks a related feature when provided. Each section below links to DEVELOPER_NOTES.md for protocol signatures, sub-protocol breakdowns, invariants, and integration recipes.

πŸ—„οΈ Store

(Required) β€” persists users, tokens, verification codes, magic links, and passkey records.

Recommended implementation:

passage-fluent β€” a Fluent-backed DatabaseStore with ready-made migrations for PostgreSQL, MySQL, and SQLite. For tests, use Passage.OnlyForTest.InMemoryStore, which ships in this repo.

Store is a composite that exposes eight sub-stores β€” one per persistence concern (users, refresh tokens, verification codes, restoration codes, magic-link tokens, exchange tokens, and the two optional passkey stores). It is the one required service because every Passage feature ultimately reads or writes through it.

Implementation guide:

See DEVELOPER_NOTES.md#store for the full sub-store list, hashing invariants, and the refresh-token rotation chain.

πŸ“§ EmailDelivery

(Optional) β€” sends verification codes, welcome emails, magic links, and password-reset emails.

Recommended implementation:

passage-mailgun β€” Mailgun-backed delivery configured with an API key, default domain, and sender identity. For SES, Postmark, Sendgrid, or other providers, conform to Passage.EmailDelivery against the provider SDK directly.

Supplying this service enables the email-side of every feature that sends mail: email verification, email-based password reset, magic-link passwordless login, and welcome emails on registration. Passage hands your implementation fully-constructed URLs, so there's no path construction on your side β€” template selection and HTML rendering are the only responsibilities.

Implementation guide:

See DEVELOPER_NOTES.md#email-delivery for the method-by-method surface and the Mailgun integration example.

πŸ“± PhoneDelivery

(Optional) β€” sends SMS verification codes and password-reset messages.

Recommended implementation:

no companion package ships yet β€” implement against Twilio, AWS SNS, Vonage, or your SMS gateway of choice.

Supplying this service enables phone-based verification and phone-based password reset. SMS messages carry raw codes rather than URLs, since users on mobile shouldn't need to click links. Message formatting β€” brand prefix, language, length β€” is entirely your implementation's choice.

Implementation guide:

See DEVELOPER_NOTES.md#phone-delivery for the three methods and a Twilio-shaped example.

🌐 FederatedLoginService

(Optional) β€” registers OAuth provider routes and resolves federated identities on callback.

Recommended implementation:

passage-imperial β€” integrates with the Imperial OAuth library to support GitHub, Google, and custom providers.

Unlike the other services, this one is a "bring a whole subsystem" contract: a single register(...) method that attaches provider routes onto Passage's router group and fires an onSignIn closure when a callback resolves. Passage uses that closure to reconcile against UserStore (linking, account-matching, creating) and to mint the exchange code the client swaps for an access token. See Sources/Passage/Features/FederatedLogin/README.md for the on-the-wire route shape.

Implementation guide:

See DEVELOPER_NOTES.md#federated-login-service for the protocol signature and the Imperial wiring example.

πŸ” PasskeyService

(Optional) β€” library-agnostic WebAuthn seam that drives all four passkey ceremony boundaries.

Recommended implementation:

passage-webauthn β€” wraps swift-webauthn. Relying-party identity and origins are configured on WebAuthnManager.Configuration, not on Passage.Configuration.Passkey.

PasskeyService is the single seam between Passage core and a concrete WebAuthn library β€” core has zero WebAuthn-library dependencies and talks only through this protocol. Providing a PasskeyService is the one gate that enables every passkey route; Passage additionally needs Store.passkeyCredentials and Store.passkeyChallenges sub-stores to be non-nil. PassageFluent.DatabaseStore will gain these alongside the upcoming passage-fluent model work; Passage.OnlyForTest.InMemoryStore already includes them for tests.

Passage exposes three distinct passkey ceremony flows (public signup, authenticated "add passkey", discoverable sign-in), with one-shot challenge storage, sign-count tracking, and opt-in Leaf templates for signup and sign-in. See the Passkey feature guide for the full route + DTO reference, trust models, and flow diagrams.

Implementation guide:

See DEVELOPER_NOTES.md#passkey-service for the four-method protocol surface, challenge-lookup invariants, and the WebAuthnPasskeyService integration example.

🎲 RandomGenerator

(Optional) β€” produces secure random tokens, verification codes, and SHA-256 hashes.

Recommended implementation:

DefaultRandomGenerator ships with Passage and is used unless you override it. Override only if you need a different code format (e.g. numeric-only codes for IVR flows) or stricter cryptographic guarantees.

The default generator emits 32-byte base64 opaque tokens, hex-encoded SHA-256 hashes, and verification codes drawn from a readability-tuned alphabet (ABCDEFGHJKLMNPQRSTUVWXYZ23456789) that eliminates the visual ambiguity of 0/O and 1/I/L. Most apps should leave this service alone.

Implementation guide:

See DEVELOPER_NOTES.md#random-generator for the protocol surface and a numeric-only override example.

🚦 Throttle

(Optional) β€” rate-limits failed authentication attempts per account and per source, per NIST SP 800-63B Β§5.2.2.

Recommended implementation:

Passage.Throttle.InMemoryService ships with Passage and is used unless you override it. It's a sliding-window counter implemented as a Swift actor β€” safe under concurrent login traffic on a single instance. For deployments that run multiple app instances behind a load balancer, supply a shared backend (e.g. Redis-backed) so counters aren't fragmented across nodes; otherwise an attacker can spread attempts across replicas to sidestep per-node caps.

Passage.Throttle.Service has three methods: check(bucket:against:at:) decides allowed vs. throttled, penalize(bucket:at:) records a failed attempt, and reset(bucket:) clears a bucket on successful authentication. Buckets key by (scope, dimension, enabled) where dimension is either .identifier(kind:value:) (per-account) or .source(String) (per IP / forwarded address).

Implementation guide:

See Sources/Passage/Services/Passage+Throttle.swift for the protocol surface and Sources/Passage/LoginThrottleMiddleware.swift for how it's wired into the login route.

Feature Discovery

Each feature maps to a directory under Sources/Passage/Features/ and is activated independently by supplying the relevant service (where required) and configuration. Expand a section to see what to wire and where to find a working example.

πŸ‘€ Account

β€” user registration, login, logout, and current-user retrieval.

Configuration

Passage.Configuration(
    // ... other config ...
    routes: .init(
        group: "auth",                                              // Base path for auth routes
        register: .init(path: "register"),                          // POST /auth/register
        login: .init(path: "login"),                                // POST /auth/login
        logout: .init(path: "logout"),                              // POST /auth/logout
        currentUser: .init(path: "me", shouldBypassGroup: true)     // GET /me
    )
)

Feature guide

See Sources/Passage/Features/Account/README.md for the full route reference, error table, and flow diagrams.

Example

See PassageExample in passage-example.

🚦 Throttling

β€” rate-limits failed login attempts per account and per source, per NIST SP 800-63B Β§5.2.2.

Service setup

Uses Passage.Throttle.Service. The default Passage.Throttle.InMemoryService is provided automatically β€” no setup required for single-instance deployments. See the Services chapter for how to override with a shared backend (Redis, DB) when running multiple app instances.

Configuration

Passage.Configuration(
    // ... other config ...
    throttle: .init(
        login: .init(
            perIdentifier: .init(maxFailures: 10, window: 15 * 60),   // 10 failures / 15 min per account
            perSource: .init(maxFailures: 20, window: 15 * 60),       // 20 failures / 15 min per IP
            enabled: true
        )
    )
)

Over-limit requests return 429 Too Many Requests with a Retry-After header (seconds until the oldest in-window attempt ages out). A successful login clears both counters for that account and source (Β§5.2.2-c). Form-validation failures (e.g. missing password) count toward the per-source bucket β€” the throttle middleware runs ahead of form decoding so malformed requests can't sidestep the counter.

Per-identifier (account) protects against sustained guessing on one account. Per-source protects against credential-stuffing sprays across many accounts. The defaults trip well before Β§5.2.2-b's 100-attempt ceiling while leaving honest users plenty of room for typos.

Feature guide

Configuration: Sources/Passage/Configuration/Configuration+Throttle.swift. Middleware: Sources/Passage/LoginThrottleMiddleware.swift. Per-identifier enforcement lives in the login service at Sources/Passage/Features/Account/Passage+Account.swift.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

βœ… Verification

β€” email and phone verification codes that confirm identifier ownership after registration.

Service setup

Requires EmailDelivery for email verification, PhoneDelivery for phone verification β€” supply either or both. See the Services chapter for integration options.

Configuration

Passage.Configuration(
    // ... other config ...
    verification: .init(
        email: .init(
            codeLength: 6,
            codeExpiration: 15 * 60,        // 15 minutes
            maxAttempts: 3
        ),
        phone: .init(
            codeLength: 6,
            codeExpiration: 5 * 60,         // 5 minutes (shorter for SMS)
            maxAttempts: 3
        ),
        useQueues: true                     // Dispatch send as Vapor Queue jobs
    )
)

Routes for each channel register only when the corresponding delivery service is provided. useQueues: true dispatches SendEmailCodeJob / SendPhoneCodeJob onto your Vapor Queues setup.

Feature guide

See Sources/Passage/Features/Verification/README.md for the verification flow, route list, and error reference.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

πŸͺ„ Passwordless

β€” magic-link authentication over email for password-free sign-in.

Service setup

Requires EmailDelivery. See the Services chapter for integration options.

Configuration

Passage.Configuration(
    // ... other config ...
    passwordless: .init(
        revokeExistingTokens: true,
        emailMagicLink: .email(
            useQueues: true,
            linkExpiration: 15 * 60,        // 15 minutes
            maxAttempts: 5,
            autoCreateUser: true,           // Create user on first successful link
            requireSameBrowser: false       // Gate verification to the requesting browser
        )
    )
)

Feature guide

See Sources/Passage/Features/Passwordless/README.md for the request/verify flow, same-browser enforcement, and token-security details.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

🎟️ Tokens

β€” JWT access tokens, opaque refresh tokens with rotation, and one-time exchange codes.

Configuration

Passage.Configuration(
    // ... other config ...
    tokens: .init(
        issuer: "https://api.example.com",                      // JWT `iss` claim
        accessToken: .init(timeToLive: 15 * 60),                // 15 minutes
        refreshToken: .init(timeToLive: 7 * 24 * 3600)          // 7 days
    ),
    jwt: .init(
        jwks: try .fileFromEnvironment()                        // JWKS file path from `JWKS_FILE_PATH`
    ),
    routes: .init(
        refreshToken: .init(path: "refresh-token"),             // POST /auth/refresh-token
        exchangeCode: .init(path: "exchange")                   // POST /auth/exchange
    )
)

JWKS loading β€” JWKS.fileFromEnvironment() reads the JWKS payload from the file path in JWKS_FILE_PATH. If you want to load the JWKS payload directly from the JWKS environment variable, use JWKS.environment() instead:

export JWKS_FILE_PATH="/path/to/jwks.json"
# or
export JWKS='{"keys":[...]}'

Refresh tokens rotate on each refresh and revoke the entire token family on reuse detection β€” see the feature guide for the rotation chain.

Feature guide

See Sources/Passage/Features/Tokens/README.md for claim definitions, rotation semantics, and exchange-code usage.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

πŸ”“ Restoration

β€” password reset via email or SMS code, with automatic refresh-token revocation on success.

Service setup

Requires EmailDelivery for email reset, PhoneDelivery for phone reset β€” supply either or both. See the Services chapter for integration options.

Configuration

Passage.Configuration(
    // ... other config ...
    restoration: .init(
        preferredDelivery: .email,          // Fallback channel for username / federated logins
        email: .init(
            codeLength: 6,
            codeExpiration: 15 * 60,        // 15 minutes
            maxAttempts: 3
        ),
        phone: .init(
            codeLength: 6,
            codeExpiration: 5 * 60,         // 5 minutes
            maxAttempts: 3
        ),
        useQueues: true                     // Dispatch send as Vapor Queue jobs
    )
)

A successful reset hashes the new password with BCrypt and revokes all refresh tokens for the user, forcing re-authentication on every device.

Feature guide

See Sources/Passage/Features/Restoration/README.md for the request/verify flow, token-revocation behavior, and error reference.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

🌐 Federated Login

β€” OAuth/OpenID Connect sign-in via Google, GitHub, Apple, or custom providers.

Service setup

Requires FederatedLoginService. Use passage-imperial for a ready-made Imperial-based implementation:

import PassageImperial

let federatedLogin = ImperialFederatedLoginService(
    services: [
        .github          : GitHub.self,
        .named("google") : Google.self,
    ]
)

Configuration

Passage.Configuration(
    // ... other config ...
    federatedLogin: .init(
        routes: .init(group: "connect"),                        // Base path: /auth/connect
        providers: [
            .init(provider: .google),                           // /auth/connect/google
            .init(provider: .github)                            // /auth/connect/github
        ],
        redirectLocation: "/dashboard"                          // Post-login redirect target
    )
)

OAuth callbacks redirect to redirectLocation with an exchange ?code=… that the client swaps for JWT tokens via POST /auth/exchange.

Feature guide

See Sources/Passage/Features/FederatedLogin/README.md for the OAuth flow, callback processing, and FederatedIdentity shape.

Example

See PassageFederatedLoginExample in passage-example.

πŸ”‘ Passkey

β€” WebAuthn / FIDO2 passkeys with public signup, authenticated "add passkey", and discoverable sign-in flows.

Service setup

Requires PasskeyService plus the two passkey sub-stores on your Store (passkeyCredentials, passkeyChallenges). Use passage-webauthn for a ready-made swift-webauthn-backed implementation:

import PassageWebAuthn
import WebAuthn

let passkeyService = WebAuthnPasskeyService(
    configuration: WebAuthnManager.Configuration(
        relyingPartyID: "example.com",
        relyingPartyName: "My App",
        relyingPartyOrigin: "https://example.com"
    )
)

Relying-party identity and allowed origin are configured on the service's underlying WebAuthnManager.Configuration, not on Configuration.Passkey.

Configuration

Passage.Configuration(
    // ... other config ...
    passkey: .init(
        routes: .init(
            guestRegistrationBegin: .default,   // opt-in: enables public guest signup
            guestRegistrationFinish: .default
        ),
        policy: .init(
            timeout: .seconds(60),
            attestation: .none,
            userVerification: .preferred,
            supportedAlgorithms: [.ES256, .RS256],
            allowDiscoverableLogin: true        // Required for the sign-in ceremony
        ),
        challengeTTL: 300                       // 5 minutes
    )
)

The two guestRegistration* routes are opt-in β€” pass .default to enable public passkey signup, omit them to require all enrollments to go through the authenticated /registration/* flow. The two registration* routes (authenticated enrollment) are always registered behind PassageGuard.

Feature guide

See Sources/Passage/Features/Passkey/README.md for the three ceremony flows, route reference, DTOs, and flow diagrams.

Example

See PassagePasskeyExample in passage-example.

πŸ”— Linking

β€” links OAuth logins to existing user accounts by matching verified email or phone.

Service setup

Requires FederatedLoginService (linking is triggered from the OAuth callback). See the Federated Login section above for the service wiring.

Configuration

Passage.Configuration(
    // ... other config ...
    federatedLogin: .init(
        providers: [.google, .github],
        accountLinking: .init(
            resolution: .automatic(
                matchBy: [.email, .phone],                      // Match only verified identifiers
                onAmbiguity: .requestManualSelection            // Fall back to manual on multiple matches
            ),
            stateExpiration: 600                                // Manual linking flow TTL (seconds)
        )
    )
)

Only verified emails and phones are considered for automatic linking β€” this prevents account takeover through unverified claims.

Feature guide

See Sources/Passage/Features/Linking/README.md for the automatic vs. manual flows, candidate-matching rules, and state storage.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

πŸ“‹ Views

β€” server-rendered Leaf templates for login, registration, password reset, magic link, account linking, and passkeys.

Configuration

Passage.Configuration(
    // ... other config ...
    views: .init(
        login: .init(
            style: .material,
            theme: .init(colors: .oceanLight),
            redirect: .init(onSuccess: "/dashboard"),
            identifier: .email
        ),
        register: .init(
            style: .material,
            theme: .init(colors: .oceanLight),
            identifier: .email
        ),
        passwordResetRequest: .init(style: .material, theme: .init(colors: .oceanLight)),
        passwordResetConfirm: .init(style: .material, theme: .init(colors: .oceanLight)),
        magicLinkRequest: .init(style: .material, theme: .init(colors: .oceanLight)),
        magicLinkVerify: .init(
            style: .material,
            theme: .init(colors: .oceanLight),
            redirect: .init(onSuccess: "/dashboard")
        ),
        linkAccountSelect: .init(style: .material, theme: .init(colors: .oceanLight)),
        linkAccountVerify: .init(style: .material, theme: .init(colors: .oceanLight))
    )
)

Four styles ship (.neobrutalism, .neomorphism, .minimalism, .material) and 17 color palettes with light/dark variants. Views are registered only when configured β€” omit a view key to skip the corresponding GET route.

Feature guide

See Sources/Passage/Features/Views/README.md for the full view list, theme options, and custom-color recipes.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

πŸͺ Hooks

β€” async lifecycle callbacks fired around authentication flows.

Configuration

Hooks are an optional fourth parameter on app.passage.configure(...), alongside services, contracts, and configuration:

try await app.passage.configure(
    services: .init(/* ... */),
    configuration: .init(/* ... */),
    hooks: .init(
        account: .hook(
            didRegisterUser: { user, request in
                request.logger.info("registered \(user.id ?? "?")")
            },
            willLoginUser: { user, request in
                guard !user.isSuspended else {
                    throw Abort(.forbidden, reason: "Account suspended")
                }
            },
            didLoginUser: { user, request in
                try? await analytics.track(.login, user: user)
            }
        )
    )
)

Hook protocols expose will* / did* pairs around each flow. will* methods are async throws and abort the flow when they throw; did* methods are non-throwing observers that fire after the underlying step has succeeded. All methods have empty default implementations, so a custom hook type only overrides the events it cares about. The .hook(...) factory shown above is convenient for ad-hoc wiring; for richer behavior conform a type to the protocol directly.

Passage.Hooks.Account β€” Account flows (register / login / logout):

Hook Fires…
willRegister Before password validation and user creation
didRegister After user creation, before the verification code is dispatched
willLogin After credentials succeed, before the session is established
didLogin After the session is established, before access tokens are issued
willLogout Before the session is cleared
didLogout After the session is cleared, before the refresh token is revoked

willLogin fires after the password check passes β€” it is the gate where you enforce post-authentication policy (account suspension, license expiry, forced MFA), not credential validation.

Passage.Hooks.Passkey β€” Passkey ceremonies (guest registration, authenticated registration, authentication). will* hooks for the finish path fire after the binding / TOCTOU / signature checks, so handlers see only ceremonies that will succeed and audit logs are not attributed to victims on hijack attempts.

Hook Fires…
willBeginGuestRegistration After identifier-already-registered check, before the WebAuthn service
didBeginGuestRegistration After the challenge is persisted (guest flow)
willBeginRegistration Before the WebAuthn service runs (authenticated flow)
didBeginRegistration After the challenge is persisted (authenticated flow)
willFinishGuestRegistration After the TOCTOU identifier check, before user creation
willFinishRegistration After the bound-user equality check passes (authenticated flow)
didFinishGuestRegistration After the credential is persisted and challenge consumed (guest flow)
didFinishRegistration After the credential is persisted and challenge consumed (authenticated)
willBeginAuthentication After the discoverable-login policy check, before the WebAuthn service
didBeginAuthentication After the authentication challenge is persisted
willFinishAuthentication After signature verification + user resolution, before sign-count update / challenge consumption / login (auth-equivalent of Account.willLogin β€” gate suspended accounts, MFA step-up, etc.)
didFinishAuthentication After the session is established and the exchange code minted

Passage.Hooks covers Account and Passkey today and is shaped to grow additional domains over time.

Feature guide

See Sources/Passage/Hooks/Passage+Hooks.swift for the container struct, Sources/Passage/Hooks/Hooks+Account.swift for the Account protocol, and Sources/Passage/Hooks/Hooks+Passkey.swift for the Passkey protocol β€” each ships default implementations and a .hook(...) factory.

Example

No dedicated example yet β€” see rozd/passage-example for the canonical walkthrough.

About

IdM framework for Vapor with JWT, multi-identifier authentication (email/phone/username), passwordless login, email/phone verification, password reset, OAuth, and account linking.

Topics

Resources

License

Stars

Watchers

Forks

Contributors