@nathapp/nestjs-auth
v4.2.3
Published
nestjs-auth
Readme
@nathapp/nestjs-auth
JWT authentication + CASL authorization library for NestJS.
Installation
npm install @nathapp/nestjs-auth @nestjs/passport@^11The command above targets NestJS 11 CommonJS applications, including Jest
consumers. Pin Passport explicitly: @nestjs/passport@12 is ESM-only and its
NestJS peer range also accepts NestJS 11, so an unversioned install can select
it and fail in a CommonJS Jest suite with Must use import to load ES Module.
Adding --experimental-vm-modules alone does not fix that combination.
| Consumer | Passport dependency |
| --- | --- |
| NestJS 10 CommonJS | @nestjs/passport@^10 or @nestjs/passport@^11 |
| NestJS 11 CommonJS / Jest | @nestjs/passport@^11 |
| NestJS 12 | @nestjs/passport@^12, with an ESM-capable runtime and test setup |
The Passport 12 peer remains supported. This repository tests the NestJS 12
line on Node 24 (>=24.9), Jest 30, and --experimental-vm-modules; the NestJS 11
baseline explicitly pins Passport 11. Both CI lines load the built CommonJS
package and authenticate/reject tokens through real Passport using
npm run test:consumer --workspace @nathapp/nestjs-auth after building.
Note for Fastify users: There is slightly different request/response handling for Fastify. Interceptors and middleware check
req.rawfor Fastify vs Express compatibility.
JWKS (Remote Identity Provider)
To verify tokens issued by an external IdP (Auth0, Cognito, Okta, Keycloak), install the optional jwks-rsa peer dependency:
npm install jwks-rsaThen configure your module:
import { Module } from '@nestjs/common';
import { AuthenticationModule, createJwksStrategyProvider } from '@nathapp/nestjs-auth';
import { MyAuthProvider } from './my-auth-provider';
@Module({
imports: [
AuthenticationModule.forRoot({
jwtOptions: {
signOption: { algorithm: 'RS256' },
issuer: 'https://my-tenant.auth0.com/',
audience: 'https://api.example.com',
jwks: {
jwksUri: 'https://my-tenant.auth0.com/.well-known/jwks.json',
// optional tuning:
// cacheMaxAge: 600000,
// jwksRequestsPerMinute: 10,
// algorithms: ['RS256'],
},
},
authProvider: MyAuthProvider,
}),
],
// Override the default JwtStrategyProvider with the JWKS factory.
// Must be registered AFTER AuthenticationModule so DI resolves this provider.
providers: [createJwksStrategyProvider()],
})
export class AppModule {}Note: Don't pass
strategyProvider: JwksJwtStrategyProviderdirectly — this monorepo usesuseFactoryregistration (see CLAUDE.md). ThecreateJwksStrategyProvider()helper wiresJWT_OPTIONS+AUTH_PROVIDERinjection for you.
JWKS Security Defaults
issueris required when using JWKS (prevents accepting tokens from any key the URI serves).algorithmsdefaults to['RS256']. HMAC algorithms (HS*) andnoneare rejected.jwksUrimust use HTTPS (setallowInsecure: trueonly for local dev against a mock server).sign()throws in JWKS mode — the strategy is verify-only.
validate<T> is an unchecked cast
DefaultJwtStrategyProvider.validate<T>() returns the principal produced by your
AuthProvider via an unchecked cast (principal as any as T). The library cannot verify
that your provider actually returns the declared type: if your AuthProvider is
typed to return IPrincipal but returns a differently shaped object, the mismatch
surfaces at runtime, not compile time. Consumers must ensure their
AuthProvider.getPrincipal() returns data compatible with the T declared at the
call site (defaults to IPrincipal).
Token purpose (typ) and refresh configuration
Access and refresh tokens carry a purpose payload claim typ: 'access' | 'refresh'
(distinct from the JWT JOSE header's typ). The built-in access provider signs object
payloads with typ: 'access'; the refresh provider always signs with typ: 'refresh'.
Verification enforces the matching purpose, so an access token can no longer be replayed
at a refresh endpoint (or vice versa) even when both use the same signing key.
Migration policy
- Existing unmarked refresh tokens stop working. A refresh token without
typ: 'refresh'is rejected byJwtRefreshStrategyand by the built-in refresh provider. There is no opt-out to accept unmarked refresh tokens. - Mint refresh tokens through the refresh strategy provider (e.g.
JwtRefreshStrategyProvider.sign({ sub })) or, for external issuers, include thetyp: 'refresh'payload claim in the token you issue. - Distinct access/refresh secrets alone are not a substitute for marking newly issued refresh tokens under this policy. Purpose is enforced independently of the key.
- Because previously issued refresh tokens cannot be re-marked, a planned rollout may require reauthentication of existing sessions before the new library is deployed.
- Refresh signing requires an object payload so the claim can be attached. Previously
accepted
string/Bufferpayloads must be converted to objects (e.g.provider.sign(String(payload))→provider.sign({ value: payload })). Access signing still accepts opaquestring/Bufferpayloads, which are treated as legacy unmarked access tokens and remain valid.
Refresh options are independent of access options
Refresh options reuse the access secrets/keys only when the refresh provides no key
material of its own, and the refresh TTL defaults to 7h regardless of the access TTL
(the access algorithm is inherited only when keys are shared and the refresh algorithm is
absent). Access TTL, issuer/audience sign claims, and unrelated validation flags are not
inherited. Configure refresh issuer/audience and their matching verification claims
explicitly when the refresh tokens need issuer/audience scoping:
AuthenticationModule.forRoot({
jwtOptions: {
secret: 'access-secret',
signOption: { expiresIn: '15m', issuer: 'my-issuer' },
issuer: 'my-issuer',
},
refreshJwtOptions: {
secret: 'refresh-secret', // or omit to share the access key
signOption: { expiresIn: '7h', issuer: 'my-issuer' },
issuer: 'my-issuer', // verification must match the signed claim
},
});