NestJS guards should verify identity before protected controllers execute.
Course navigationPhase 18⌄
NestJS Authentication Guards
Make NestJS independently verify Supabase access tokens and attach trusted identity before protected handlers run.
Phase outcome and project increment
Learning outcome
Make NestJS independently verify Supabase access tokens and attach trusted identity before protected handlers run.
Concrete deliverable
Authentication guard + verification service + current-user helper + protected /auth/me and task routes.
Principles to keep while coding
Derive the authenticated user from a verified token, never from a user id supplied by the browser.
Attach validated identity to the request in one controlled place so downstream code can consume it consistently.
Return 401 for missing or invalid authentication and reserve 403 for authenticated callers lacking permission.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Add Authorization: Bearer token handling in the API client without putting tokens in URLs/logs.
- 02
Extract and validate the bearer scheme in a guard with clear missing/malformed behavior.
- 03
Verify the token server-side using the supported Supabase/JWT verification path.
- 04
Normalize verified claims into an application user identity object.
- 05
Attach identity once and expose it through a typed decorator/helper rather than reparsing tokens in services.
- 06
Protect /auth/me and one task endpoint; test valid, missing, malformed, expired, and wrong-project tokens.
Failures you should know how to diagnose
Trusting userId from request body/query
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Decoding JWT without signature verification
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Returning 403 when no valid authentication exists
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Logging bearer tokens during debugging
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
From Session to Trusted API Identity
PHASE 18 - BOOK 01 From Session to Trusted API Identity Why a Next.js login is not enough for NestJS Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access token
Next.js to NestJS Bearer Token Handoff
PHASE 18 - BOOK 02 Next.js -> NestJS Bearer Token Handoff Sending the session access token without leaking it into URLs or logs Phase 18 trust boundary Next.js authenticated session Authoriz
Server Side Supabase Token Verification
PHASE 18 - BOOK 03 Server-Side Supabase Token Verification Using getClaims() in a NestJS 12-compatible service Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <ac
Build the NestJS AuthGuard
PHASE 18 - BOOK 04 Build the NestJS AuthGuard Turning verification into a reusable request gate Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access token> Nes
Typed Identity and CurrentUser Decorator
PHASE 18 - BOOK 05 Typed Identity and @CurrentUser() Making verified identity ergonomic without hiding trust boundaries Phase 18 trust boundary Next.js authenticated session Authorization: B
Protect Routes and Default Deny Strategy
PHASE 18 - BOOK 06 Protect Routes Deliberately Per-route, controller-wide, and auth-by-default strategies Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access
401 Error Semantics and Client Recovery
PHASE 18 - BOOK 07 401 Error Semantics Making authentication failures predictable and safe Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access token> NestJS a
CORS Authorization Headers and Preflight
PHASE 18 - BOOK 08 CORS and Authorization Headers Why browser bearer requests can trigger preflight Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access token>
Signing Keys JWKS and 2026 Supabase Choices
PHASE 18 - BOOK 09 Signing Keys, JWKS, Rotation and 2026 Supabase Choices Understanding what getClaims() is doing for you Phase 18 trust boundary Next.js authenticated session Authorization:
Testing Debugging and Observability
PHASE 18 - BOOK 10 Testing and Debugging the Guard Proving the trust boundary instead of assuming it works Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access
Hands On Lab and Phase Exit
PHASE 18 - BOOK 11 Hands-On Lab and Phase Exit Implement, verify, explain, then prepare for authorization Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access
Phase 18 Architecture Review
PHASE 18 - BOOK 12 Phase 18 Architecture Review From verified identity to Phase 19 authorization Phase 18 trust boundary Next.js authenticated session Authorization: Bearer <access token> Ne
Guided Build Lab
Purpose Make NestJS independently verify Supabase access tokens and attach trusted identity before protected handlers run.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 18 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 18 without following a transcript.
Exercises that increase independence
Add Authorization: Bearer token handling in the API client without putting tokens in URLs/logs.
Verify the token server-side using the supported Supabase/JWT verification path.
reproduce and diagnose — Trusting userId from request body/query.
deliver Authentication guard + verification service + current-user helper + protected /auth/me and task routes.
Can explain why Next.js login is not enough for the API and Can trace guard execution.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_NestJS_Auth_Guard_Snapshot/PHASE_18_TEST_MATRIX.md
# Phase 18 test matrix | Scenario | Expected | |---|---| | No Authorization header | 401 | | Wrong scheme | 401 | | Malformed JWT | 401 | | Expired JWT | 401 | | Token from wrong Supabase project | 401 | | Valid current access token | 200 and verified identity | | Protected tasks without token | 401 | | Health endpoint | 200 without token |
reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/.env.example
SUPABASE_URL=https://<project-ref>.supabase.co SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/install-auth-api.ps1
# Run from apps/api pnpm add @supabase/supabase-js@2.116.0
reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/auth/auth.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { CurrentUser } from '../common/decorators/current-user.decorator'
import type { AuthenticatedUser } from './authenticated-user'
import { SupabaseAuthGuard } from './supabase-auth.guard'
@Controller('auth')
export class AuthController {
@UseGuards(SupabaseAuthGuard)
@Get('me')
me(@CurrentUser() user: AuthenticatedUser) {
return { user }
}
}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/auth/auth.module.ts
import { Module } from '@nestjs/common'
import { AuthController } from './auth.controller'
import { SupabaseAuthGuard } from './supabase-auth.guard'
import { SupabaseAuthService } from './supabase-auth.service'
@Module({
controllers: [AuthController],
providers: [SupabaseAuthService, SupabaseAuthGuard],
exports: [SupabaseAuthService, SupabaseAuthGuard],
})
export class AuthModule {}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/auth/authenticated-user.ts
export type AuthenticatedUser = {
id: string
email?: string
role?: string
sessionId?: string
}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/auth/supabase-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
import type { FastifyRequest } from 'fastify'
import { SupabaseAuthService } from './supabase-auth.service'
import type { AuthenticatedUser } from './authenticated-user'
type AuthenticatedRequest = FastifyRequest & { user?: AuthenticatedUser }
@Injectable()
export class SupabaseAuthGuard implements CanActivate {
constructor(private readonly auth: SupabaseAuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<AuthenticatedRequest>()
const value = request.headers.authorization
const [scheme, token] = value?.split(' ') ?? []
if (scheme !== 'Bearer' || !token) {
throw new UnauthorizedException('Bearer access token required')
}
const claims = await this.auth.verifyAccessToken(token)reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/auth/supabase-auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { createClient } from '@supabase/supabase-js'
@Injectable()
export class SupabaseAuthService {
private readonly url: string
private readonly client
constructor(config: ConfigService) {
this.url = config.getOrThrow<string>('SUPABASE_URL')
const key = config.getOrThrow<string>('SUPABASE_PUBLISHABLE_KEY')
this.client = createClient(this.url, key, {
auth: {
persistSession: false,
autoRefreshToken: false,
},
})
}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/common/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import type { FastifyRequest } from 'fastify'
import type { AuthenticatedUser } from '../../auth/authenticated-user'
type AuthenticatedRequest = FastifyRequest & { user: AuthenticatedUser }
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
return ctx.switchToHttp().getRequest<AuthenticatedRequest>().user
},
)reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/tasks/tasks.controller.auth-example.ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { SupabaseAuthGuard } from '../auth/supabase-auth.guard'
@Controller('tasks')
@UseGuards(SupabaseAuthGuard)
export class TasksController {
@Get()
findAll() {
// Existing Phase 15 implementation remains here.
}
}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/api/src/tasks/tasks.module.ts
import { Module } from '@nestjs/common'
import { AuthModule } from '../auth/auth.module'
import { TasksController } from './tasks.controller'
import { TasksService } from './tasks.service'
@Module({
imports: [AuthModule],
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/apps/web/src/lib/api/authenticated-fetch.ts
import { createClient } from '@/lib/supabase/server'
export async function authenticatedApiFetch(path: string, init: RequestInit = {}) {
const supabase = await createClient()
const { data, error } = await supabase.auth.getSession()
if (error || !data.session?.access_token) {
throw new Error('Authenticated session required')
}
const headers = new Headers(init.headers)
headers.set('Authorization', `Bearer ${data.session.access_token}`)
return fetch(`${process.env.API_BASE_URL}${path}`, { ...init, headers, cache: 'no-store' })
}reference/TaskFlow_NestJS_Auth_Guard_Snapshot/tests/auth-guard-smoke-tests.ps1
$api = 'http://127.0.0.1:4000'
Write-Host '1) No token -> expect 401'
try { Invoke-RestMethod -Uri "$api/auth/me" } catch { $_.Exception.Response.StatusCode.value__ }
Write-Host '2) Malformed token -> expect 401'
try { Invoke-RestMethod -Uri "$api/auth/me" -Headers @{ Authorization = 'Bearer not-a-jwt' } } catch { $_.Exception.Response.StatusCode.value__ }
Write-Host '3) Valid current development token -> expect 200'
$token = Read-Host 'Paste a current DEVELOPMENT access token'
Invoke-RestMethod -Uri "$api/auth/me" -Headers @{ Authorization = "Bearer $token" }