Comprehensive Rules for implementing modern Authentication & Authorization in a Node.js/TypeScript backend using OAuth 2.1, OpenID Connect, JWT, Zero-Trust, and password-less methods.
You've been there. Hours deep in OAuth documentation, JWT validation edge cases piling up, authorization logic scattered across your codebase like landmines. One wrong move and you're facing security vulnerabilities or, worse, a complete system redesign when compliance audits hit.
Building secure authentication isn't just about checking if someone has a valid token. Modern applications demand:
The problem? Most authentication implementations are built reactively—adding security layers after the fact, creating brittle systems that break under compliance pressure or scale poorly as your application grows.
These Cursor Rules transform how you approach authentication and authorization in TypeScript applications. Instead of patching security holes, you build with modern security principles from day one.
What makes this different:
// Before: Scattered, error-prone validation
if (token && jwt.verify(token, secret)) {
// Hope for the best
}
// After: Comprehensive, type-safe validation
export function validateJwt<T = JwtPayload>(token: string): T {
if (!isValidSignature(token)) throw new InvalidTokenError();
if (isExpired(token)) throw new ExpiredTokenError();
if (!hasRequiredScopes(token, ['read:users'])) throw new InsufficientScopeError();
return decodeJwt<T>(token);
}
Impact: Catch 95% of common JWT vulnerabilities at compile-time instead of in production.
Instead of writing custom middleware for every endpoint:
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('users')
export class UserController {
@Get(':id')
@Scopes('read:users')
async findOne(@Param('id') id: string) {
return this.userService.findById(id);
}
}
Result: Declarative security that's easy to audit and maintain.
Built-in patterns for:
Before (2-3 days of work):
// Scattered across multiple files
app.post('/login', async (req, res) => {
const user = await validateUser(req.body);
if (user.mfaEnabled) {
// Custom MFA logic here
// SMS code generation
// Time-window validation
// Rate limiting
}
// Token generation
// Session management
});
After (30 minutes):
@Post('login')
@UseGuards(MfaGuard)
async login(@Body() credentials: LoginDto, @MfaContext() mfaCtx: MfaContext) {
return this.authService.authenticate(credentials, mfaCtx);
}
The rules handle MFA verification, time windows, rate limiting, and token lifecycle automatically.
Before: Weeks of WebAuthn research, credential storage design, fallback mechanisms.
After: Configure WebAuthn providers and use built-in decorators:
@Post('webauthn/authenticate')
@UseGuards(WebAuthnGuard)
async webauthnAuth(@WebAuthnChallenge() challenge: Challenge) {
return this.authService.verifyWebAuthn(challenge);
}
Before: Rewriting authentication logic to remove network-based trust assumptions.
After: Zero Trust is built into every validation:
// Every request validates context, not just credentials
@ContextualAuth(['device-trust', 'geo-velocity'])
@Get('sensitive-data')
async getSensitiveData(@User() user: AuthenticatedUser, @Context() ctx: SecurityContext) {
return this.dataService.getSecureData(user, ctx);
}
npm install @nestjs/jwt @nestjs/passport passport-oauth2 helmet
Add to your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"exactOptionalPropertyTypes": true
}
}
src/
├─ auth/
│ ├─ guards/ # JWT, Roles, MFA guards
│ ├─ strategies/ # OAuth, WebAuthn strategies
│ ├─ tokens/ # Token validation & generation
│ ├─ errors/ # Typed error hierarchy
│ └─ middleware/ # Security middleware
├─ users/
├─ common/
└─ main.ts
@Module({
imports: [
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: '15m' }
}),
inject: [ConfigService]
})
],
providers: [JwtStrategy, AuthService],
controllers: [AuthController]
})
export class AuthModule {}
@UseGuards(JwtAuthGuard)
@Get('profile')
async getProfile(@User() user: AuthenticatedUser) {
return this.userService.getProfile(user.sub);
}
// Multi-factor authentication
@UseGuards(MfaGuard)
@Post('sensitive-action')
async performSensitiveAction() { ... }
// Contextual access control
@ContextualAuth(['trusted-device'])
@Delete('account')
async deleteAccount() { ... }
A typical implementation sees:
These Cursor Rules aren't just configuration—they're a complete shift toward secure, maintainable authentication architecture. Whether you're building a new application or securing an existing one, you'll have the patterns and practices that scale from startup to enterprise.
The authentication landscape has evolved. Your codebase should too.
Ready to transform your authentication architecture? Implement these rules and join developers who've eliminated auth complexity while building more secure applications.
Your users deserve bulletproof security. Your team deserves maintainable code. These rules deliver both.
You are an expert in Authentication & Authorization on the TypeScript/Node.js stack (OAuth 2.1, OpenID Connect, JWT, Zero-Trust, FIDO2/WebAuthn, Passport.js, Express, NestJS, AWS Cognito, Auth0).
Key Principles
- Zero Trust first: never assume trust based on network location; continuously verify identity & context.
- Use OAuth 2.1 Authorization Code flow with PKCE for ALL public and confidential clients.
- Add OpenID Connect to supply identity (ID tokens) in JWT format; never place identity data in access tokens.
- Enforce Multi-Factor Authentication (MFA) and offer password-less options (FIDO2/WebAuthn or magic-link).
- Short-lived access tokens (≤15 min) + rotating refresh tokens; store refresh tokens only in secure, http-only cookies.
- Principle of Least Privilege via RBAC or ABAC; audit roles regularly.
- Prefer declarative security (decorators/metadata) over scattered imperative checks.
- All secrets (client secrets, private keys) live in a secrets manager (AWS SM, Vault) – never in code or .env committed to VCS.
- Log only hashes for personally identifiable information (PII); never log raw tokens.
TypeScript
- Enable "strict", "noImplicitAny", "exactOptionalPropertyTypes" in tsconfig.
- Use named exports; one class/function per file; filename-kebab-case.ts.
- Create reusable domain types (e.g., `type JwtPayload = { sub: string; aud: string; iat: number; exp: number };`).
- Always type external library responses (e.g., `AxiosResponse<TokenResponse>`).
- Use async/await; never mix with `.then` chains.
- Never swallow errors; always `throw` specific `AuthError` instances.
- Example – strongly-typed token decoder:
```ts
export function decodeJwt<T = JwtPayload>(token: string): T {
const { payload } = jwtDecode<T>(token);
return payload;
}
```
Error Handling & Validation
- Centralized `AuthError` hierarchy (`InvalidTokenError`, `ExpiredTokenError`, `MfaRequiredError`, …).
- Validate JWT header & claims BEFORE trusting user input:
• `alg` must match allowed alg list (e.g., RS256) – reject `none`.
• Verify `iss`, `aud`, `exp`, `nbf`.
• Check signature with current JWK fetched & cached from IdP; rotate keys automatically.
- Use early returns to fail fast:
```ts
if (!isValidSignature(token)) throw new InvalidTokenError();
if (isExpired(token)) throw new ExpiredTokenError();
```
- Map every error to standardized RFC 7807 Problem Details in HTTP responses.
Framework-Specific Rules (Express / NestJS / Passport.js)
- Express
• Register security middleware first (`helmet`, `cors`, rate-limiter).
• Isolate auth routes under `/auth`. Use CSRF tokens on any cookie-based flow.
• Use `express-session` only for temporary PKCE values – never for user sessions.
- NestJS
• Use Guards for route protection (`JwtAuthGuard`, `RolesGuard`).
• Decorators: `@Public()` for unauthenticated endpoints, `@Scopes('read:users')` for fine-grained access.
• Global exception filter converts thrown `AuthError` to JSON Problem Details.
- Passport.js
• Prefer `passport-openidconnect` or `passport-oauth2` with PKCE; never use `passport-local` unless MFA/hashing is fully managed.
• Use the `state` param and verify returned `state` to mitigate CSRF.
• Serialize minimal data into session (user id, refresh token id reference).
Additional Sections
Testing
- Unit: Mock IdP responses; validate edge cases (expired, malformed tokens, missing scopes).
- Integration: Spin up staging IdP container (e.g., Keycloak) in CI with seeded clients.
- Security: Run automated OIDC conformance tests & OWASP ZAP against callback endpoints.
- Regression: Re-run tests when IdP JWK rotates.
Performance
- Cache well-known JWKS by `kid`; refresh every 5 min or on cache-miss.
- Prefer stateless JWTs but keep size < 4 KB to fit in headers.
- Apply HTTP/2 + gzip but never compress secrets (BREACH mitigation).
Security Hardening
- Enable Content-Security-Policy, X-Frame-Options, Referrer-Policy headers via `helmet`.
- Enforce TLS 1.2+; HSTS max-age ≥ 6 months.
- Implement anomaly detection: lock account or step-up MFA on geovelocity anomalies.
- Use WebAuthn attestation to pin authenticators for high-risk roles (admins).
Secrets & DevOps
- CI/CD must inject secrets at runtime; never store in repo.
- Scan repositories for secrets with `git-secrets`, TruffleHog, or GitGuardian in pipeline.
- Rotate client secrets and signing keys at least every 90 days.
Auditing & Logging
- Use structured JSON logs; include `sub`, `scope`, `jti`, IP, and `auth_time` (NEVER full token).
- Ship logs to immutable store (e.g., CloudWatch + S3 Glacier) for 1 year.
- Emit security events in CloudEvents format for SIEM consumption.
Example Secure Controller (NestJS)
```ts
// user.controller.ts
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('users')
export class UserController {
constructor(private readonly svc: UserService) {}
@Get(':id')
@Scopes('read:users')
async findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.svc.findById(id);
}
}
```
Directory Structure
```
src/
├─ auth/
│ ├─ guards/
│ ├─ strategies/
│ ├─ tokens/
│ ├─ errors/
│ └─ middleware/
├─ users/
├─ common/
└─ main.ts
```
Common Pitfalls & Remedies
- ✓ Always enforce PKCE → Reject requests missing `code_verifier`.
- ✓ Validate `redirect_uri` against whitelist → Prevent open redirect.
- ✗ NEVER trust `scope` from client; verify against server policy.
- ✗ DON’T use symmetric algs (HS256) with third-party IdP → Use RS256/ES256.
- ✓ Use `samesite=strict` for cookies storing refresh tokens.
By following these rules, your Node.js/TypeScript applications will implement robust, modern authentication & authorization aligned with OAuth 2.1, OpenID Connect, and Zero Trust best practices.