JWT access token issuance, opaque refresh token management, and token exchange flows.
The Tokens feature manages authentication tokens throughout their lifecycle. It handles issuing JWT access tokens for API authentication, rotating refresh tokens for session continuity, and one-time exchange codes for OAuth redirect flows. Token rotation with family revocation provides security against token theft.
Key capabilities:
- JWT access tokens with standard claims (sub, exp, iat, iss, aud)
- Opaque refresh tokens with automatic rotation
- One-time exchange codes for OAuth callbacks
- Family-based token revocation on reuse detection
Passage.Configuration(
// ... other config ...
tokens: .init(
issuer: "https://api.example.com", // JWT issuer claim
accessToken: .init(timeToLive: 15 * 60), // 15 minutes
refreshToken: .init(timeToLive: 7 * 24 * 3600) // 7 days
),
routes: .init(
refreshToken: .init(path: "refresh-token"), // POST /auth/refresh-token
exchangeCode: .init(path: "exchange") // POST /auth/exchange
)
)| Option | Type | Default | Description |
|---|---|---|---|
tokens.issuer |
String? |
nil |
JWT iss claim value |
tokens.accessToken.timeToLive |
TimeInterval |
900 (15 min) |
Access token validity duration |
tokens.refreshToken.timeToLive |
TimeInterval |
604800 (7 days) |
Refresh token validity duration |
Short-lived JWT for API authentication. Validated on each request via PassageBearerAuthenticator.
Claims:
| Claim | Description |
|---|---|
sub |
User ID |
exp |
Expiration timestamp |
iat |
Issued at timestamp |
iss |
Token issuer (optional) |
aud |
Intended audience (optional) |
scope |
Authorization scope (optional) |
Long-lived opaque token stored hashed in database. Used to obtain new access tokens without re-authentication.
Properties:
- Stored as SHA256 hash (plain text never persisted)
- Linked to user via
userId - Tracks rotation chain via
replacedById - Supports revocation via
revokedAt
One-time code for OAuth redirect flows. Allows secure token handoff after browser-based OAuth.
Properties:
- TTL: 60 seconds
- Single-use: Consumed immediately on exchange
- Enables API clients to receive tokens after OAuth callback
- Generate JWT access token with user claims
- Generate random opaque refresh token
- Hash refresh token and store in database
- Return
AuthUserwith both tokens
- Hash incoming refresh token
- Look up in database by hash
- Validate: not expired, not revoked, not replaced
- If invalid → revoke entire token family (security)
- Generate new access + refresh tokens
- Mark old token as replaced by new one
- Return new
AuthUser
- Find all refresh tokens for user
- Mark as revoked
- User must re-authenticate
Passage implements refresh token rotation to detect token theft:
flowchart LR
A[Token A<br/>issued] -->|refresh| B[Token B<br/>rotated]
B -->|refresh| C[Token C<br/>current]
A -.->|reuse attempt| D[REVOKE ALL]
B -.->|reuse attempt| D
Security behavior:
- Each refresh issues a new refresh token
- Old token marked as replaced (
replacedById) - Reusing a replaced token → entire family revoked
- Detects stolen tokens being used after legitimate refresh
| Method | Default Path | Auth | Description |
|---|---|---|---|
| POST | /auth/refresh-token |
No | Exchange refresh token for new tokens |
| POST | /auth/exchange |
No | Exchange one-time code for tokens |
Request:
{
"refreshToken": "opaque-refresh-token-string"
}Response:
{
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"refreshToken": "new-opaque-refresh-token",
"tokenType": "Bearer",
"expiresIn": 900,
"user": {
"id": "user-uuid",
"email": "user@example.com",
"phone": null
}
}Request:
{
"code": "exchange-code-from-redirect"
}Response: Same format as refresh token.
sequenceDiagram
participant Client
participant App
participant Passage
participant Store
Note over Client,Store: Token Refresh Flow
Client->>App: POST /refresh-token
App->>Passage: refresh(token)
Passage->>Passage: Hash token
Passage->>Store: Find by hash
Store-->>Passage: Refresh token record
alt Token reused (already replaced)
Passage->>Store: Revoke entire family
Passage-->>App: Error: invalid token
else Token valid
Passage->>Passage: Generate new tokens
Passage->>Store: Create new refresh token
Passage->>Store: Mark old as replaced
Passage-->>App: AuthUser
end
App-->>Client: New tokens
| Error | Trigger |
|---|---|
refreshTokenNotFound |
Token hash not found in database |
invalidRefreshToken |
Token expired, revoked, or already replaced |
Refresh tokens are stored with:
tokenHash- SHA256 hash of opaque tokenuserId- Owner of the tokenexpiresAt- Expiration timestamprevokedAt- When revoked (null if active)replacedById- ID of replacement token (rotation chain)
Used by OAuth/federated login to pass tokens via URL:
- OAuth callback completes authentication
- Server generates exchange code (
createExchangeCode) - Redirect to client with
?code=xxx - Client POSTs to
/auth/exchange - Server validates code, returns full tokens
- Code marked as consumed (single-use)
- Account - Login issues tokens, logout revokes them
- Restoration - Password reset revokes all tokens
- Federated Login - OAuth flows use exchange codes