@fluojs/passport
v2.0.2
Published
Strategy-agnostic authentication guard wiring and strategy registry for Fluo.
Maintainers
Readme
@fluojs/passport
Node.js support is >=24.0.0 <27. See Node.js support and migration before upgrading.
Strategy-agnostic auth execution layer for fluo. It routes any AuthStrategy through a generic AuthGuard into the request context, populating requestContext.principal.
Table of Contents
Installation
npm install @fluojs/passportWhen to Use
- When you need to protect routes with authentication and authorization (RBAC/Scopes).
- When using multiple auth strategies (e.g., JWT, Cookies, API Keys) in the same application.
- When you need a bridge to existing Passport.js strategies.
- When implementing refresh token rotation or account-linking policies.
Quick Start
1. Register Modules
Use the built-in bearer JWT preset and register it with PassportModule.forRoot(...).
import { Module } from '@fluojs/core';
import { JwtModule } from '@fluojs/jwt';
import {
BEARER_JWT_STRATEGY_NAME,
BearerJwtStrategy,
createBearerJwtStrategyRegistration,
PassportModule,
} from '@fluojs/passport';
@Module({
imports: [
JwtModule.forRoot({
algorithms: ['HS256'],
audience: 'my-app',
issuer: 'my-api',
secret: 'your-secure-secret',
}),
PassportModule.forRoot(
{ defaultStrategy: BEARER_JWT_STRATEGY_NAME },
[createBearerJwtStrategyRegistration()],
),
],
providers: [BearerJwtStrategy],
})
export class AuthModule {}BearerJwtStrategy is the first-party strategy preset for Authorization: Bearer <token> credentials. JWT-based passport strategies require both pieces of module wiring: JwtModule.forRoot(...) registers DefaultJwtVerifier, the providers entry exposes BearerJwtStrategy to that verifier through DI, and PassportModule.forRoot(...) registers the named strategy that @UseAuth('jwt') resolves. Returning the DefaultJwtVerifier.verifyAccessToken(...) result unchanged preserves the normalized principal contract (subject, claims, issuer, audience, roles, and scopes) that AuthGuard writes to requestContext.principal.
2. Protect Routes
Use @UseAuth() to enforce authentication and @RequireScopes() to enforce authorization.
import { Controller, Get, type RequestContext } from '@fluojs/http';
import { UseAuth, RequireScopes } from '@fluojs/passport';
@Controller('/profile')
export class ProfileController {
@Get('/')
@UseAuth('jwt')
@RequireScopes('profile:read')
async getProfile(input: never, ctx: RequestContext) {
return { user: ctx.principal };
}
}Common Patterns
Bearer JWT Preset
Use the built-in BearerJwtStrategy when your app authenticates Authorization: Bearer <token> requests; no application-owned parsing code is required.
import { Module } from '@fluojs/core';
import { JwtModule } from '@fluojs/jwt';
import {
BEARER_JWT_STRATEGY_NAME,
BearerJwtStrategy,
createBearerJwtStrategyRegistration,
PassportModule,
} from '@fluojs/passport';
@Module({
imports: [
JwtModule.forRoot({
algorithms: ['HS256'],
audience: 'my-app',
issuer: 'my-api',
secret: 'your-secure-secret',
}),
PassportModule.forRoot(
{ defaultStrategy: BEARER_JWT_STRATEGY_NAME },
[createBearerJwtStrategyRegistration()],
),
],
providers: [BearerJwtStrategy],
})
export class AuthModule {}Register BearerJwtStrategy as a provider in the same application module that imports PassportModule and JwtModule; JwtModule remains the owner of verification configuration, and the preset does not introduce a combined JWT/Passport module facade. The preset registers under the stable strategy name BEARER_JWT_STRATEGY_NAME ('jwt'), so protected routes keep using @UseAuth('jwt'), and createBearerJwtStrategyRegistration() returns the matching AuthStrategyRegistration.
Credential extraction is strict. The credential must match RFC 6750 b64token: one or more ASCII letters, digits, -, ., _, ~, +, or /, followed only by optional trailing = padding. It rejects ?, internal =, Unicode, whitespace, and control characters before verification. A missing or empty Authorization header throws AuthenticationRequiredError, and when the adapter surfaces an array-valued Authorization header only the first entry is read. A wrong-scheme or malformed header throws AuthenticationFailedError; the scheme match is case-insensitive and uses one or more ASCII spaces. Missing and malformed credentials serialize the bare WWW-Authenticate: Bearer challenge. Verification failures keep DefaultJwtVerifier error causality: expired credentials throw AuthenticationExpiredError, and JwtInvalidTokenError credentials throw AuthenticationFailedError, each preserving the original JWT error as cause and serializing WWW-Authenticate: Bearer error="invalid_token". JwtConfigurationError and verifier-provider infrastructure failures propagate unchanged instead of becoming credential failures. AuthGuard maps only credential failures to the canonical 401 Unauthorized response, and scope mismatches still produce 403 Forbidden.
BearerJwtStrategy returns the normalized JwtPrincipal from DefaultJwtVerifier.verifyAccessToken(...) unchanged, preserving subject, claims, issuer, audience, roles, and scopes.
A custom AuthStrategy remains the extension point for token revocation, account-state checks, alternate extraction, and application-owned policy.
Passport.js Bridge
Easily adapt any standard Passport.js strategy (like passport-google-oauth20) to work with fluo's DI and async lifecycle.
const googleBridge = createPassportJsStrategyBridge('google', GoogleStrategy, {
mapPrincipal: ({ user }) => ({ subject: user.id, claims: user }),
});createPassportJsStrategyBridge(...) is an intentionally documented manual-composition compatibility helper. It returns the provider bundle and matching AuthStrategyRegistration needed by PassportModule.forRoot(...); applications should register the bridge providers in the same module that imports PassportModule and pass googleBridge.strategy to the strategy registry:
@Module({
imports: [
PassportModule.forRoot({ defaultStrategy: 'google' }, [googleBridge.strategy]),
],
providers: [GoogleStrategy, ...googleBridge.providers],
})
export class AuthModule {}This bridge helper is the official exception to the module-facade rule for Passport.js adapters because third-party strategy instances must be bound as providers before AuthGuard can execute them. It does not replace PassportModule, @UseAuth(...), or AuthGuard as the application-facing authentication surface.
The bridge settles each Passport.js strategy execution exactly once. A strategy must call one of the bound Passport actions (success, fail, redirect, pass, or error); promise rejections, promise completion without an action, and callback-style executions that exceed the bounded action timeout become authentication failures instead of leaving the request unresolved. The bridge never consumes an authenticate() return value: only a bound action settles the request. Any AuthStrategyResult with handled: true is fully terminal after the strategy commits a response, even if it also includes a principal; AuthGuard skips principal validation, scope checks, requestContext.principal assignment, and the protected handler. Custom mapPrincipal functions must return a valid fluo Principal with a non-empty subject and object claims.
The bridge calls authenticate(request, options) with the active platform adapter's raw host request when one exists, and with the normalized fluo request otherwise. It never creates a Passport-initialized host request, so strategies that depend on Passport middleware augmentation (request.logIn, request.user, session state) or on adapter-specific request fields need the compatibility checklist in NestJS → fluo Migration Map before cutover.
actionTimeoutMs defaults to 30_000 milliseconds and must be a non-negative finite number. Set it to 0 to schedule timeout settlement on the next timer turn. Negative, NaN, and infinite values throw RangeError when the bridge strategy is constructed instead of disabling the settlement bound.
Application shutdown cancels every in-flight bridge execution, clears its action timeout, and rejects the pending authentication instead of retaining request state through the configured timeout. The bridge participates in the ordinary application lifecycle, so this cancellation runs when the application or application context is closed.
Cookie Auth Preset
Use CookieAuthModule.forRoot(...) when your app authenticates requests from HTTP cookies.
import { Module } from '@fluojs/core';
import { JwtModule } from '@fluojs/jwt';
import { CookieAuthModule } from '@fluojs/passport';
@Module({
imports: [
CookieAuthModule.forRoot({
accessTokenCookieName: 'session_access',
refreshTokenCookieName: 'session_refresh',
cookieOptions: { path: '/sessions' },
}),
JwtModule.forRoot({
algorithms: ['HS256'],
global: true,
secret: 'your-secure-secret',
}),
],
})
export class AuthModule {}Import CookieAuthModule.forRoot(...) and JwtModule.forRoot(...) together for cookie-auth support. CookieAuthModule owns one PassportModule registry that registers CookieAuthStrategy, CookieManager, and AuthGuard. To combine cookie auth with bearer or a Passport.js bridge, pass the other named registrations as the third forRoot argument instead of importing a sibling PassportModule.forRoot(...); keep the additional strategy providers application-owned. CookieAuthModule and JwtModule are sibling imports, so set the documented global: true JWT option to make DefaultJwtVerifier visible when the cookie strategy resolves.
Pass cookie names once to CookieAuthModule.forRoot(...): the same configuration supplies CookieAuthStrategy's credential reader and CookieManager's writer and clearer. For a separately owned object outside DI, use CookieManager.create(config) with the same CookieManagerConfig. See the cookie preset migration for removed manual helpers.
CookieAuthStrategy preserves the normalized JWT principal contract from @fluojs/jwt, including subject, claims, issuer, audience, roles, and scopes.
Cookie access tokens must be non-empty strings. Missing cookies can resolve to { authenticated: false } only when requireAccessToken: false; malformed present cookie values always fail authentication before JWT verification.
Cookie verification failures keep their documented classification: expired access tokens raise AuthenticationExpiredError, invalid access tokens raise AuthenticationFailedError, and missing or malformed access-token cookies raise AuthenticationRequiredError. The originating @fluojs/jwt error is preserved as the cause, while AuthGuard still answers HTTP 401 for every variant.
CookieManagerConfig.cookieOptions accepts SetCookieOptions. Its accessTokenTtlSeconds and refreshTokenTtlSeconds fields supply the default Max-Age for the matching token cookie when the positional TTL argument is omitted; an explicit positional TTL wins, then the matching configured TTL, then cookieOptions.maxAge. maxAge is measured in seconds and is forwarded to portable HTTP maxAgeSeconds, not the millisecond convention used by some host frameworks. Auth cookies default to Path=/, Secure, HttpOnly, and SameSite=Strict, with no default domain or lifetime; general @fluojs/http setCookie(...) does not add those auth defaults.
CookieManager appends access-token and refresh-token Set-Cookie values without overwriting cookies that were already placed on the response, even when the underlying adapter stores the existing header with different casing such as set-cookie. It uses the portable HTTP serializer, so cookie values are percent-encoded before they are emitted and malformed cookie names or attributes fail validation instead of emitting invalid headers.
Protected routes must keep using @UseAuth(...). If you configure requireAccessToken: false, a missing cookie resolves to an explicit unauthenticated result instead of an anonymous principal, so protected routes still reject the request.
Use @UseOptionalAuth(...) only on routes that intentionally support both signed-in and guest callers:
import { Controller, Get, type RequestContext } from '@fluojs/http';
import { UseOptionalAuth } from '@fluojs/passport';
@Controller('/session')
export class SessionController {
@Get('/')
@UseOptionalAuth('cookie')
getSession(_input: never, ctx: RequestContext) {
return { subject: ctx.principal?.subject ?? null };
}
}Refresh Token Lifecycle
The package provides a built-in RefreshTokenStrategy plus the RefreshTokenModule and RefreshTokenService contract for secure token rotation and revocation.
import { Module } from '@fluojs/core';
import { Controller, Post, type RequestContext } from '@fluojs/http';
import { JwtModule } from '@fluojs/jwt';
import {
PassportModule,
REFRESH_TOKEN_STRATEGY_NAME,
RefreshTokenModule,
RefreshTokenStrategy,
UseAuth,
} from '@fluojs/passport';
@Controller('/auth')
export class AuthController {
@Post('/refresh')
@UseAuth('refresh-token')
async refresh(input: never, ctx: RequestContext) {
return ctx.principal; // Contains new token pair
}
}
@Module({
controllers: [AuthController],
imports: [
JwtModule.forRoot({
algorithms: ['HS256'],
global: true,
refreshToken: {
expiresInSeconds: 604800,
rotation: true,
secret: 'your-refresh-token-secret',
store: 'memory', // Use an application-owned atomic durable store in production.
},
secret: 'your-access-token-secret',
}),
RefreshTokenModule.forRoot(),
PassportModule.forRoot(
{ defaultStrategy: REFRESH_TOKEN_STRATEGY_NAME },
[{ name: REFRESH_TOKEN_STRATEGY_NAME, token: RefreshTokenStrategy }],
),
],
})
export class AuthModule {}Import JwtModule.forRoot(...), RefreshTokenModule.forRoot(), and PassportModule.forRoot(...) together. JwtModule is the sole owner of refresh-token crypto, expiry, rotation, and store configuration. The global: true option makes its exported RefreshTokenService visible to the sibling RefreshTokenModule, whose default registration aliases that service rather than constructing another service or store. The explicit 'memory' store is development-only opt-in; production must provide one application-owned atomic RefreshTokenStore. RefreshTokenStrategy is the default endpoint recipe and PassportModule registers its @UseAuth('refresh-token') name.
RefreshTokenStrategy extracts body.refreshToken before Authorization: Bearer <token>; a present malformed body value fails rather than falling back to the header. Missing credentials become AuthenticationRequiredError, malformed credentials and JWT reuse/invalidity become AuthenticationFailedError, and expired JWT refresh tokens become AuthenticationExpiredError.
A successful exchange resolves ctx.principal to the RefreshTokenPrincipal shape: the rotated pair is nested under claims.accessToken and claims.refreshToken, with the verified subject at the top level. The separate exported RefreshTokenAuthResult type describes the application-facing exchange payload a refresh endpoint returns to clients.
RefreshTokenStrategy reads tokens from body.refreshToken, Authorization: Bearer ..., or x-refresh-token; malformed non-string tokens fail authentication. After rotation, it verifies the returned access token through DefaultJwtVerifier and uses its normalized subject. A custom RefreshTokenServicePort may own refresh state, but it must be registered alongside a globally visible JwtModule configuration and return access tokens that configuration accepts; it is not a non-JWT exchange path.
Account Linking and Status
Use createConservativeAccountLinkPolicy(...) and resolveAccountLinking(...) to model identity-link decisions. The default conservative policy links one unambiguous existing account link or user-confirmed matches, reports multiple existing links as conflicts, and otherwise creates, skips, rejects, or reports conflicts deterministically.
createPassportPlatformStatusSnapshot(...) and createPassportPlatformDiagnosticIssues(...) expose readiness/health diagnostics for registered strategies, default strategy configuration, presets, and refresh-token store readiness.
Public API Overview
Decorators
@UseAuth(strategyName): AttachesAuthGuardand sets the active strategy.@UseOptionalAuth(strategyName): AttachesAuthGuardbut allows routes without scopes to continue when the strategy reports missing credentials.@RequireScopes(...scopes): Enforces specific scope requirements.
Module and Guard Entry Points
PassportModule: Module entry point for passport strategy wiring.AuthGuard: The HTTP guard that executes the strategy chain and enforces required scopes.PassportModuleOptions,AuthStrategyRegistration,AuthStrategyRegistry,AuthGuardContract: Strategy registry and guard wiring contracts.
Strategy Contracts and Errors
AuthStrategy: The contract for implementing custom authentication logic.AuthRequirement: Route-level metadata for the selected strategy, optional-auth mode, and required scopes.AuthStrategyResult,AuthOptionalResult,AuthHandledResult: Strategy return variants for principals, intentionally missing credentials, and fully handled responses.AuthStrategyResolutionError,AuthenticationRequiredError,AuthenticationFailedError,AuthenticationExpiredError: Public errors used by guards and strategy adapters for registry misses, missing credentials, invalid credentials, and expired credentials.isPassportError(...)validates their versioned owner contract across compatible duplicate copies; guards preserve the same HTTP 401 classification without accepting plain code-string lookalikes.
Metadata and Scope Helpers
defineAuthRequirement(...),getOwnAuthRequirement(...),getAuthRequirement(...): Public helpers for reading and writing auth requirement metadata when integrating custom decorators or tooling withAuthGuard.- Scope requirements are normally authored with
@RequireScopes(...); lower-level scope normalization helpers remain internal and are not part of the package root export.
Bearer JWT Preset
BearerJwtStrategy,BEARER_JWT_STRATEGY_NAME: Built-in bearer credential strategy and its stable registration name ('jwt').- Bearer helper:
createBearerJwtStrategyRegistration.
Cookie Auth Preset
CookieAuthModule: Module entry point for the built-in cookie-auth preset.CookieAuthStrategy,COOKIE_AUTH_STRATEGY_NAME,COOKIE_AUTH_OPTIONS,DEFAULT_COOKIE_AUTH_OPTIONS,DEFAULT_COOKIE_OPTIONS: Cookie strategy wiring tokens, preset defaults, and response-cookie defaults.CookieAuthOptions,CookieManagerConfig,CookieOptions,SetCookieOptions: Cookie strategy and response cookie configuration types.CookieManagerConfig.cookieOptionsacceptsSetCookieOptions, whose per-token TTL fields become default cookieMax-Agevalues.CookieManager: Class-token-preserving utility for setting and clearing HttpOnly access/refresh token cookies. UseCookieManager.create(config)for separately owned instances.normalizeCookieAuthOptions: Low-level cookie option normalization helper.
Refresh Token Preset
RefreshTokenModule: Module entry point for the built-in refresh-token preset.RefreshTokenStrategy,REFRESH_TOKEN_STRATEGY_NAME,REFRESH_TOKEN_SERVICE: Refresh-token strategy and service alias wiring.RefreshTokenServicePort,RefreshTokenInput,RefreshTokenAuthResult,RefreshTokenPrincipal: Custom refresh-service port, exchange payload shapes, and the principal shape resolved ontoctx.principalafter a successful exchange. Custom services still require a globally visibleJwtModuleverifier and must return an access token it accepts. The JWT-owned concreteRefreshTokenServiceis imported from@fluojs/jwt.- Refresh helpers:
createRefreshTokenStrategyRegistration.
Refresh ownership migration
Move every refresh secret, expiresInSeconds, rotation, and store value into JwtModule.forRoot({ global: true, refreshToken: ... }), then replace RefreshTokenModule.forRoot(JwtRefreshTokenAdapter) with RefreshTokenModule.forRoot(). Remove imports of JwtRefreshTokenAdapter, REFRESH_TOKEN_MODULE_OPTIONS, and RefreshTokenModuleOptions. Consumers that used Passport's former structural RefreshTokenService type should use RefreshTokenService from @fluojs/jwt for the canonical path. A custom RefreshTokenServicePort remains supported only with a globally visible JwtModule verifier and JWT access tokens that it accepts.
Passport.js Bridge
createPassportJsStrategyBridge(...): Compatibility helper that adapts Passport.js strategies to fluoAuthStrategyand returns providers plus the matching strategy registration forPassportModule.forRoot(...).PassportJsAuthStrategy,PassportJsStrategyLike,PassportJsPrincipalMapperInput,PassportJsPrincipalMapper,PassportJsAuthStrategyOptions,PassportJsStrategyBridge: Bridge strategy, mapper, configuration, and provider bundle contracts.
Account Linking
ACCOUNT_LINKING_POLICY: DI token for registering an account-linking policy implementation.createConservativeAccountLinkPolicy(...),resolveAccountLinking(...): Conservative default policy and resolver for identity-link decisions.AccountIdentity,AccountLinkCandidate,AccountLinkAttempt,AccountLinkContext,AccountLinkPolicy,AccountLinkPolicyDecision,AccountLinkingOptions,AccountLinkingResolution: Account-linking input, policy, and result contracts.AccountLinkConflictError,AccountLinkRejectedError: Errors raised for ambiguous or rejected account-linking attempts.
Status and Diagnostics
createPassportPlatformStatusSnapshot(...): Creates a runtime platform snapshot for strategy registry, preset readiness, ownership, and telemetry labels.createPassportPlatformDiagnosticIssues(...): Emits diagnostic issues for empty registries, missing default strategies, cookie preset readiness, and refresh-token backing store readiness.PassportPlatformStatusSnapshot,PassportStatusAdapterInput: Status helper input/output contracts.
UseOptionalAuth only bypasses missing credentials when no scopes are required; scoped routes still need a principal. AuthHandledResult with handled: true is terminal only after the strategy commits the response, including results that also carry a principal. Passport.js bridge redirect() commits the response and skips the protected handler, while pass() and strategy completion without a Passport action are authentication failures. Refresh-token backing store status and diagnostic surfaces redact secret-like reason strings before exposing readiness, health, details, or diagnostic causes.
Related Packages
@fluojs/jwt: The underlying token core for JWT-based strategies.@fluojs/http: Provides the routing and guard infrastructure.
Example Sources
packages/passport/src/guard.test.ts: Guard execution and scope enforcement patterns.packages/passport/src/adapters/passport-js.ts: Implementation of the Passport.js bridge.packages/passport/src/bearer/bearer-jwt.ts: Built-in bearer JWT strategy preset.
Compatible service copies
AuthGuard supports ordinary class injection across compatible same-realm copies, including the UseAuth/guard path; strategy registries remain isolated.
