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. Not yet production-ready.
- 🔐 User Registration & Login - Complete authentication flow with secure password hashing
- 📧 Email Authentication - Email-based identifier with verification codes
- 📱 Phone Authentication - Phone number identifier with SMS verification (requires custom implementation of
PhoneDeliveryservice) - 👤 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)
- 🔗 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
- 🎨 Fully Customizable - Configure routes, tokens, templates, and behavior
Add Passage to your Package.swift:
dependencies: [
// 🛂 Authentication and user management for Vapor.
.package(url: "https://github.com/vapor-community/passage", branch: "main"),
]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"),- Set a custom working directory in your scheme and point it to your project folder.
- Create a JWKS file
keypair.jwksand place it in the root of your project. - 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
)
)
)
)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.
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
The Store protocol is the only required service you must provide. It handles all persistence for users, identifiers, tokens, and verification codes.
Recommended Implementation: Use the passage-fluent package, which provides a complete Fluent-based storage implementation with migrations for PostgreSQL, MySQL, and SQLite.
Testing Implementation: The PassageOnlyForTest module provides an in-memory store for testing purposes.
import PassageFluent
let store = DatabaseStore(app: app, db: app.db)Or implement your own by conforming to Passage.Store, which composes four sub-stores:
UserStore- User account managementTokenStore- Refresh token storage and rotationCodeStore- Email/phone verification codesResetCodeStore- Password reset codes
The EmailDelivery protocol handles sending verification codes and password reset emails. Implement this to enable email-based features.
Recommended Implementation: Use the passage-mailgun package for Mailgun integration.
import PassageMailgun
let emailDelivery = MailgunEmailDelivery(
app: app,
configuration: .init(
mailgun: .init(
apiKey: "your-mailgun-api-key",
defaultDomain: .init("mg.example.com", .us)
),
sender: .init(
email: "[email protected]",
name: "No Reply"
),
)
)Or implement your own email provider by conforming to the Passage.EmailDelivery protocol.
The PhoneDelivery protocol handles sending SMS verification codes and password reset messages. Implement this to enable phone-based authentication.
Example implementation using Twilio, AWS SNS, or other SMS providers:
struct TwilioPhoneDelivery: Passage.PhoneDelivery {
func send(code: String, to phone: String, on request: Request) async throws {
// Send SMS via your preferred provider
}
}The FederatedLoginService protocol enables OAuth-based authentication with providers like Google, GitHub, Facebook, etc.
Recommended Implementation: Use the passage-imperial package, which integrates with the Imperial OAuth library.
import PassageImperial
try await app.passage.configure(
services: .init(
store: store,
federatedLogin: ImperialFederatedLoginService(
services: [
.github : GitHub.self,
.named("google") : Google.self,
]
)
),
configuration: .init(
origin: URL(string: "https://api.example.com")!,
federatedLogin: .init(
routes: .init(),
providers: [
.github(
credentials: .conventional
),
.google(
credentials: .conventional,
scope: ["profile", "email"]
)
]
)
)
)The RandomGenerator protocol generates secure verification codes and tokens. A default implementation is provided using Swift's RandomNumberGenerator, so you typically don't need to implement this yourself.
Implement a custom generator only if you need specific code formats or cryptographic requirements:
struct CustomRandomGenerator: Passage.RandomGenerator {
func generateCode(length: Int) -> String {
// Custom code generation logic
}
}Configure Passage behavior through the Passage.Configuration struct:
try await app.passage.configure(
services: services,
configuration: .init(
// Base origin URL for your API
origin: URL(string: "https://api.example.com")!,
// Customize route paths
routes: .init(
group: "auth", // Base path (default: "auth")
register: .init(path: "register"),
login: .init(path: "login"),
logout: .init(path: "logout"),
refreshToken: .init(path: "refresh-token"),
currentUser: .init(path: "me")
),
// Configure token lifetimes
tokens: .init(
issuer: "https://api.example.com",
accessToken: .init(
timeToLive: 900 // 15 minutes
),
refreshToken: .init(
timeToLive: 2_592_000 // 30 days
),
),
// JWT/JWKS configuration
jwt: .init(
jwks: try .fileFromEnvironment(), // Load from JWKS env var or file
),
// Passwordless authentication (magic links)
passwordless: .init(
emailMagicLink: .email(
autoCreateUser: true,
requireSameBrowser: true
)
),
// Email/Phone verification settings; providing an `EmailDelivery` or `PhoneDelivery` service enables verification
verification: .init(
email: .init(
codeLength: 6,
codeExpiration: 600,
maxAttempts: 5
),
phone: .init(
codeLength: 6,
codeExpiration: 600,
maxAttempts: 5
),
useQueues: true // Global queue setting
),
// Password reset settings; as with verification, providing `EmailDelivery` or `PhoneDelivery` enables password reset
restoration: .init(
preferredDelivery: .email,
email: .init(
codeLength: 6,
codeExpiration: 600,
maxAttempts: 5
)
useQueues: true
),
// Federated Login configuration
federatedLogin: .init(
providers: [
.github(
credentials: .conventional
),
.google(
credentials: .conventional,
scope: ["profile", "email"]
)
],
accountLinking: .init(
resolution: .automatic(
matchBy: [.email, .phone],
// Links accounts automatically by matching identifiers;
// falls back to manual linking when multiple matches exist
onAmbiguity: .requestManualSelection
)
)
),
// Web form views (Leaf templates)
views: .init(
register: .init(/* ... */),
login: .init(/* ... */),
passwordResetRequest: .init(/* ... */)
)
)
)- Routes: Customize all endpoint paths (registration, login, logout, token refresh, user info, verification, password reset)
- Tokens: Set TTLs for access and refresh tokens
- JWT/JWKS: Configure issuer, audience, and load JWKS from environment or file
- Verification: Enable/disable email/phone verification, set code TTLs, enable async queue processing
- Restoration: Configure password reset flows for email/phone
- Passwordless: Configure magic link authentication with link expiration, auto-create users, and same-browser verification
- OAuth: Define providers and callback routes
- Views: Enable web forms with customizable Leaf templates
Load JWKS from environment variable or file:
# Option 1: Environment variable
export JWKS='{"keys":[...]}'
# Option 2: File path
export JWKS_FILE_PATH="/path/to/jwks.json"jwt: .init(jwks: try .fileFromEnvironment())