JWT Provider
Problem
You want every protected route to have a fully populated ctx.user, derived from a JWT in the Authorization: Bearer <token> header. If the token is missing, expired, or invalid, the request should be rejected with 401.
Solution
A single @Injectable() provider that combines JwtService with the fromHeader extractor.
// src/auth/providers/jwt-auth.provider.ts
import { Injectable, inject, UnauthorizedException, type RequestContext } from '@miiajs/core'
import { fromHeader, type AuthProvider } from '@miiajs/auth'
import { JwtService, type JwtPayload } from '@miiajs/jwt'
import { UsersService } from '../../users/users.service.js'
@Injectable()
export class JwtAuth implements AuthProvider {
private jwtService = inject(JwtService)
private usersService = inject(UsersService)
private extract = fromHeader()
async authenticate(ctx: RequestContext) {
const token = this.extract(ctx)
if (!token) throw new UnauthorizedException('Missing token')
let payload: JwtPayload
try {
payload = await this.jwtService.verify(token)
} catch {
throw new UnauthorizedException('Invalid token')
}
const user = await this.usersService.findById(payload.sub as number)
if (!user) throw new UnauthorizedException('User no longer exists')
return user
}
}
That's it. Fifteen lines of logic, fully typed, fully injectable.
Wire it up
Register the provider in your auth module:
// src/auth/auth.module.ts
import { Module } from '@miiajs/core'
import { JwtAuth } from './providers/jwt-auth.provider.js'
@Module({
providers: [JwtAuth],
})
export class AuthModule {}
Apply the guard to any controller or route:
// src/users/users.controller.ts
import { Controller, Get, UseGuard, type RequestContext } from '@miiajs/core'
import { AuthGuard } from '@miiajs/auth'
import { JwtAuth } from '../auth/providers/jwt-auth.provider.js'
@Controller('/users')
@UseGuard(AuthGuard(JwtAuth))
export class UsersController {
@Get('/me')
me(ctx: RequestContext) {
return ctx.user
}
}
Declaring ctx.user
@miiajs/core intentionally does not pre-declare user on RequestContext - the package has no opinion on what an authenticated subject looks like. Before you can touch ctx.user anywhere, augment the interface once in your project with the type your provider returns:
// src/types/auth.d.ts
import type { User } from '../users/users.schema.js'
declare module '@miiajs/core' {
interface RequestContext {
user?: User
}
}
Now ctx.user is User | undefined everywhere - and inside guarded routes you can safely treat it as User.
Variations
Custom header
private extract = fromHeader('x-auth-token', '')
// reads X-Auth-Token: <raw token>
Cookie-based
private extract = fromCookie('access_token')
// reads Cookie: access_token=<token>
Query string (download links)
private extract = fromQuery('token')
// reads ?token=<jwt>
You can also accept multiple sources with a fallback:
private extractHeader = fromHeader()
private extractQuery = fromQuery('token')
async authenticate(ctx: RequestContext) {
const token = this.extractHeader(ctx) ?? this.extractQuery(ctx)
if (!token) throw new UnauthorizedException('Missing token')
// ...
}
Security notes
- Pin the signing algorithm. Configure
JwtModulewith a fixedalgand useallowedAlgorithmson verify - otherwise an attacker can forge tokens by swapping to a weaker algorithm (or tonone). See@miiajs/jwtfor the options. - Short-lived tokens. Sign with
expiresInset to minutes-to-hours, not days. Use a refresh-token flow or server-side session for longer-lived access. - Revocation. JWTs are stateless, so logout is best-effort - you need either short TTLs (the common answer) or a denylist checked on verify.
- Clock skew.
josetolerates smalliat/nbf/expskew by default; be explicit about it if your verifiers and signers run on different machines.
See also
@miiajs/auth-AuthProviderinterface,AuthGuard, extractors@miiajs/jwt-JwtServiceconfiguration and API- Local Provider - username/password login
- OAuth2 Provider - GitHub login