Passage – an identity and authentication framework for Vapor 4

Passage – an identity and authentication framework for Vapor 4
Hi everyone, I'd like to introduce Passage, an identity management and authentication framework for Vapor applications. It has recently moved under the vapor-community organisation, and I'd love to get feedback from this community before the API settles. Status: alpha (0.5.x). It's functional and heavily tested, but the API is still subject to change before 1.0. Please don't treat this as "ready, ship it" – treat it as "ready, please tell me what's wrong with it." The problem it solves Every server-side project needs the same authentication substrate: registration, login, password hashing, token issuance and refresh, email/phone verification, password reset, OAuth, and – increasingly – passkeys. Every project also gets to reimplement it, and every reimplementation is a fresh opportunity to get token rotation or account linking subtly wrong. Passage aims to provide that substrate as a Vapor-native library: secure defaults out of the box, and a protocol seam anywhere you need to substitute your own infrastructure. What it looks like // configure.swift app.views.use(.leaf) app.middleware.use(app.sessions.middleware) try await app.passage.configure( services: .init( store: DatabaseStore(app: app, db: app.db), emailDelivery: MailgunEmailDelivery(...), phoneDelivery: nil ), configuration: .init( origin: URL(string: "https://example.com")!, sessions: .init(enabled: true), jwt: .init(jwks: try .fileFromEnvironment()), tokens: .init( issuer: "https://api.example.com", accessToken: .init(timeToLive: 15 * 60), refreshToken: .init(timeToLive: 7 * 24 * 3600) ) ) ) // routes.swift 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))!" } That's a working auth backend: routes for register / login / logout / refresh / me, plus server-rendered Leaf pages at /auth/register and /auth/login if you want them. Every route path, TTL, and template is configurable; features that need a service you didn't supply simply don't register their routes. Standards compliance, not just "secure by default" This is the part I'd most like scrutiny on. Passage targets NIST SP 800-63B Authenticator Assurance Level 1 – https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL – and ships an executable compliance suite under Tests/PassageTests/AAL1/. Each of those tests cites the specific clause it asserts in its name, so the suite doubles as a machine-checked compliance ledger rather than a prose claim in a README: §5.1.1 – memorized secrets (length, composition rules, breach-list behaviour) §7.1 – session management §4.1.3 / §7.2 – reauthentication §5.2.2 – throttling of online guessing attacks Alongside it, docs/AAL1/ carries the claim statement, a threat model, a conformance matrix, and a revision process for when the interpretation of a clause changes. If you think a clause is being read too generously – or that a passing test doesn't actually demonstrate what it claims – that's exactly the feedback I'm after. The concrete behaviours that fall out of this: BCrypt password hashing, opaque refresh tokens stored hashed with rotation and family-wide revocation on reuse detection, verification codes hashed before storage, one-shot WebAuthn challenges persisted as SHA-256 rather than plaintext, per-account and per-source login throttling returning 429 with Retry-After, and password reset revoking every refresh token for that user. Features Account – registration, login, logout, current user Tokens – JWT access tokens with JWKS, opaque refresh tokens with rotation, one-time exchange codes Verification – email and phone codes with configurable length, TTL, and attempt caps Restoration – password reset over email or SMS Passwordless – email magic links, with optional same-browser enforcement and auto-create-on-first-login Passkeys (WebAuthn) – three ceremonies: public guest signup, authenticated "add a passkey", and discoverable sign-in Federated login – OAuth/OIDC via Google, GitHub, or custom providers Account linking – automatic linking on verified email/phone match, with manual fallback on ambiguity Throttling – sliding-window rate limiting per account and per source Views – Leaf templates for login, registration, reset, magic link, linking, and passkeys; 4 styles × 17 themes with light/dark variants Hooks – async will* / did* lifecycle callbacks; throw from a will* hook to gate a flow on your own policy (suspended accounts, licence checks, MFA step-up) Architecture: six protocols, and nothing else The core Passage target depends only on Vapor, JWT, Leaf, Queues, swift-crypto, swift-log, and NIO. No ORM, no WebAuthn library, no mail SDK. Everything else arrives through six service protocols: Protocol Required? Companion package Store passage-fluent (Postgres / MySQL / SQLite) EmailDelivery optional passage-mailgun PhoneDelivery optional – (bring Twilio / SNS / Vonage) FederatedLoginService optional passage-imperial PasskeyService optional passage-webauthn (wraps swift-webauthn) RandomGenerator / Throttle.Service optional sensible defaults ship in-package Only Store is mandatory. Supplying an optional service is what enables the corresponding feature – no separate feature flags to keep in sync. Two consequences worth calling out: Passkeys are library-agnostic. PasskeyService is a four-method protocol covering the ceremony boundaries. passage-webauthn implements it over swift-webauthn, but core has no idea that library exists, and swapping it costs one type. You can keep your existing user table. passage-fluent runs in two modes. Island mode creates its own users/identifiers tables around a built-in model. Overlay mode lets you conform your app's existing Fluent user model to PassageUserModel – any IDValue (UUID, Int, String) – and inject it, so Passage adopts your schema instead of demanding its own. That was the single most-requested capability from early users. There's also Passage.OnlyForTest.InMemoryStore, shipped as a separate product, so you can integration-test your auth routes without a database. Testing ~1,500 tests across unit, integration, and the AAL1 conformance suite; CI and Codecov badges are on the README. Swift 6.3, strict concurrency, macOS 13+ / Linux, Vapor 4.121+, MIT licensed. What I'm looking for Honest, specific criticism, particularly on: API shape before 1.0. The services / configuration / hooks split, and whether Configuration has grown past the point where nested initialisers are pleasant to write. The AAL1 claim. Under-tested clauses, over-generous readings, or clauses I've missed. The Store protocol. It's a composite of eight sub-stores. Is that the right granularity for someone implementing a non-Fluent backend? Missing features. TOTP/MFA and AAL2 are the obvious next frontier – I'd like to know what else is blocking adoption for you. Links: Core: https://github.com/vapor-community/passage Examples: https://github.com/rozd/passage-example Issues and discussions are open, and PRs are very welcome. Thanks for reading – and thanks to the Vapor community for taking the project in. 1 post - 1 participant Read full topic

Take Your Experience to the Next Level

New

Download our mobile app for a faster and better experience.

Comments

0
U

Join the discussion

Sign in to leave a comment

0:000:00