Auth

Strategy-based authentication with JWT support, token extractors, and the @Public decorator.

@miiajs/auth provides a strategy-based authentication system with built-in JWT and local (username/password) strategies.

Installation

npm install @miiajs/auth

Quick setup

import { Module } from '@miiajs/core'
import { ConfigModule, ConfigService } from '@miiajs/config'
import { JwtModule } from '@miiajs/auth/jwt'

@Module({
  imports: [
    ConfigModule.configure({ schema: EnvSchema }),
    JwtModule.configure((resolve) => ({
      secret: resolve(ConfigService).getOrThrow('JWT_SECRET'),
      expiresIn: '1h',
    })),
  ],
  controllers: [AuthController],
  providers: [MyJwtStrategy, MyLocalStrategy],
})
class AppModule {}

Strategies

A strategy is a class that validates incoming requests. Define one with the @Strategy() decorator:

import { Strategy } from '@miiajs/auth'
import type { AuthStrategy } from '@miiajs/auth'

@Strategy('api-key')
class ApiKeyStrategy implements AuthStrategy {
  async validate(ctx: RequestContext) {
    const key = ctx.req.headers.get('x-api-key')
    if (key !== 'my-secret') throw new UnauthorizedException()
    return { type: 'api' }
  }
}

JWT strategy

Extend JwtStrategy for Bearer token authentication:

import { Strategy } from '@miiajs/auth'
import { JwtStrategy, fromHeader } from '@miiajs/auth/jwt'
import type { JwtPayload } from '@miiajs/auth/jwt'

@Strategy('jwt')
class MyJwtStrategy extends JwtStrategy {
  extractToken = fromHeader() // Authorization: Bearer <token>

  protected authenticate(payload: JwtPayload, ctx: RequestContext) {
    return { id: payload.sub, role: payload.role }
  }
}

The authenticate() method is optional — by default it returns the raw JWT payload.

Local strategy

Extend LocalStrategy for username/password authentication:

import { Strategy } from '@miiajs/auth'
import { LocalStrategy } from '@miiajs/auth/local'

@Strategy('local')
class MyLocalStrategy extends LocalStrategy {
  protected usernameField = 'email' // default: 'username'

  private userService = inject(UserService)

  async authenticate(email: string, password: string) {
    const user = await this.userService.findByEmail(email)
    if (!user || !await verify(user.passwordHash, password)) {
      throw new UnauthorizedException('Invalid credentials')
    }
    return { id: user.id, role: user.role }
  }
}

AuthGuard

Use AuthGuard() to protect routes with a strategy:

import { AuthGuard } from '@miiajs/auth'
import { UseGuard } from '@miiajs/core'

@Controller('/users')
@UseGuard(AuthGuard()) // defaults to 'jwt'
class UserController {
  @Get('/')
  list() { return [] }

  @Post('/login')
  @UseGuard(AuthGuard('local'))
  login(ctx: RequestContext) {
    // ctx.user is set by the strategy
    return { user: ctx.user }
  }
}

@Public

Mark routes as public to skip authentication:

import { Public } from '@miiajs/auth'

@Controller('/posts')
@UseGuard(AuthGuard())
class PostController {
  @Get('/')
  @Public() // No auth required
  list() { return [] }

  @Post('/')
  create(ctx: RequestContext) {
    // Auth required
    return ctx.body
  }
}

@Public() can also be applied at the class level to make all routes public.

JwtService

Sign and verify JWT tokens:

import { JwtService } from '@miiajs/auth/jwt'

@Injectable()
class AuthService {
  private jwt = inject(JwtService)

  async login(user: { id: string; role: string }) {
    const token = await this.jwt.sign(
      { sub: user.id, role: user.role },
      { expiresIn: '7d' },
    )
    return { token }
  }

  async verify(token: string) {
    return this.jwt.verify(token)
  }
}

Sign options

interface JwtSignOptions {
  secret?: string           // Override module secret
  algorithm?: string        // e.g. 'HS256', 'RS256'
  expiresIn?: string | number // e.g. '1h', 3600
  subject?: string
  issuer?: string
  audience?: string
}

Verify options

interface JwtVerifyOptions {
  secret?: string
  algorithms?: string[]
  issuer?: string
  audience?: string
}

JwtModule configuration

JwtModule.configure({
  secret: 'my-secret',     // Required for HMAC algorithms
  algorithm: 'HS256',      // Default
  expiresIn: '1h',         // Default
  issuer: 'my-app',        // Optional
  audience: 'my-api',      // Optional
})

Or with DI factory:

JwtModule.configure((resolve) => ({
  secret: resolve(ConfigService).getOrThrow('JWT_SECRET'),
  expiresIn: '1h',
}))

Token extractors

Control where the JWT token is read from:

import { fromHeader, fromCookie, fromQuery } from '@miiajs/auth/jwt'

@Strategy('jwt')
class MyJwtStrategy extends JwtStrategy {
  // Authorization: Bearer <token> (default)
  extractToken = fromHeader()

  // Custom header: X-Auth-Token: <token>
  extractToken = fromHeader('x-auth-token', '')

  // Cookie: access_token=<token>
  extractToken = fromCookie('access_token')

  // Query string: ?token=<jwt>
  extractToken = fromQuery('token')
}

Complete example

// auth.controller.ts
@Controller('/auth')
class AuthController {
  private jwt = inject(JwtService)

  @Post('/login')
  @UseGuard(AuthGuard('local'))
  async login(ctx: RequestContext) {
    const token = await this.jwt.sign({
      sub: ctx.user.id,
      email: ctx.user.email,
      role: ctx.user.role,
    })
    return { token }
  }
}

// user.controller.ts
@Controller('/users')
@UseGuard(AuthGuard())
class UserController {
  private userService = inject(UserService)

  @Get('/')
  @Public()
  findAll() {
    return this.userService.findAll()
  }

  @Get('/:id')
  findOne(ctx: RequestContext) {
    return this.userService.findById(ctx.params.id)
  }
}

Exports

// @miiajs/auth
import { AuthGuard, Strategy, Public, isPublic } from '@miiajs/auth'

// @miiajs/auth/jwt
import { JwtModule, JwtService, JwtStrategy, fromHeader, fromCookie, fromQuery } from '@miiajs/auth/jwt'

// @miiajs/auth/local
import { LocalStrategy } from '@miiajs/auth/local'