Course navigationPhase 18
Volume 05 · Phase 18

NestJS Authentication Guards

Make NestJS independently verify Supabase access tokens and attach trusted identity before protected handlers run.

16lessons + labs
5klesson words
13source references
What you will ship

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.

Figure 18.1 — Where this phase lives in TaskFlow
Next.js session
Bearer access token
NestJS guard
Supabase/JWT verification
trusted request user
controller/service
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

NestJS guards should verify identity before protected controllers execute.

02

Derive the authenticated user from a verified token, never from a user id supplied by the browser.

03

Attach validated identity to the request in one controlled place so downstream code can consume it consistently.

04

Return 401 for missing or invalid authentication and reserve 403 for authenticated callers lacking permission.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Add Authorization: Bearer token handling in the API client without putting tokens in URLs/logs.

  2. 02

    Extract and validate the bearer scheme in a guard with clear missing/malformed behavior.

  3. 03

    Verify the token server-side using the supported Supabase/JWT verification path.

  4. 04

    Normalize verified claims into an application user identity object.

  5. 05

    Attach identity once and expose it through a typed decorator/helper rather than reparsing tokens in services.

  6. 06

    Protect /auth/me and one task endpoint; test valid, missing, malformed, expired, and wrong-project tokens.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

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.

Failure drill 2

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.

Failure drill 3

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.

Failure drill 4

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.

Watch-order style

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.

01

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

2 min
02

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

2 min
03

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

2 min
04

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

2 min
05

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

2 min
06

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

2 min
07

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

2 min
08

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>

2 min
09

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:

2 min
10

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

2 min
11

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

2 min
12

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

2 min
LAB

Guided Build Lab

Purpose Make NestJS independently verify Supabase access tokens and attach trusted identity before protected handlers run.

3 min
LAB

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.

2 min
LAB

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?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 18 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Add Authorization: Bearer token handling in the API client without putting tokens in URLs/logs.

Practice

Verify the token server-side using the supported Supabase/JWT verification path.

Debug

reproduce and diagnose — Trusting userId from request body/query.

Independent

deliver Authentication guard + verification service + current-user helper + protected /auth/me and task routes.

Explain-back

Can explain why Next.js login is not enough for the API and Can trace guard execution.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 18's engineering goal?
02Which practice best matches Phase 18's engineering goal?
03Which practice best matches Phase 18's engineering goal?
04Which practice best matches Phase 18's engineering goal?
05You are working through the Phase 18 lab. Which next action is the most disciplined step?
06You are working through the Phase 18 lab. Which next action is the most disciplined step?
07You are working through the Phase 18 lab. Which next action is the most disciplined step?
08You are working through the Phase 18 lab. Which next action is the most disciplined step?
Source snapshot

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" }