Overview
@miiajs/auth is a thin layer of primitives for building authentication into MiiaJS applications. It does not ship built-in JWT, OAuth, or password logic - those are application-level concerns. Instead, it gives you three things:
AuthProvider- an interface describing a class that authenticates a request.AuthGuard(...Providers)- a guard factory that runs one or more providers.- HTTP utilities - token extractors, constant-time string comparison.
For JWT signing and verification, see @miiajs/jwt. For end-to-end examples, see the recipes linked below.
Installation
bun add @miiajs/auth
npm install @miiajs/auth
pnpm add @miiajs/auth
yarn add @miiajs/auth
AuthProvider
A provider is a regular @Injectable() class that reads a request and returns the authenticated subject - or throws an HttpException.
import { Injectable, inject, UnauthorizedException, type RequestContext } from '@miiajs/core'
import { fromHeader, type AuthProvider } from '@miiajs/auth'
import { JwtService } from '@miiajs/jwt'
import { UsersService } from './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')
const payload = await this.jwtService.verify<{ sub: number }>(token).catch(() => {
throw new UnauthorizedException('Invalid token')
})
const user = await this.usersService.findById(payload.sub)
if (!user) throw new UnauthorizedException('User not found')
return user
}
}
Register it like any other provider:
@Module({ providers: [JwtAuth, UsersService] })
class AuthModule {}
AuthGuard
AuthGuard(...Providers) builds a guard that tries each provider in order and assigns the first success to ctx.user.
import { AuthGuard } from '@miiajs/auth'
import { UseGuard, Controller, Get, type RequestContext } from '@miiajs/core'
import { JwtAuth } from './providers/jwt-auth.js'
@Controller('/users')
@UseGuard(AuthGuard(JwtAuth))
class UserController {
@Get('/me')
me(ctx: RequestContext) {
return ctx.user
}
}
Multi-provider (OR)
Pass more than one provider class for fall-back authentication. The first provider that succeeds wins, and its result becomes ctx.user. If a provider throws an HttpException, the guard moves on to the next one. Any other error (TypeError, ReferenceError, etc.) is treated as a programming bug and propagates immediately - it is never silently swallowed.
@UseGuard(AuthGuard(JwtAuth, ApiKeyAuth))
If every provider throws an HttpException, the last error is rethrown.
AND semantics
Stack guards on top of each other to require multiple checks:
@UseGuard(AuthGuard(JwtAuth), Roles('admin'))
Skipping
@SkipGuard(AuthGuard) (referencing the factory itself) removes the auth guard from a specific route at compile time - zero runtime cost.
import { SkipGuard } from '@miiajs/core'
@Controller('/posts')
@UseGuard(AuthGuard(JwtAuth))
class PostController {
@Get('/')
@SkipGuard(AuthGuard)
list() {
return [] // public
}
}
Token extractors
import { fromHeader, fromCookie, fromQuery } from '@miiajs/auth'
fromHeader() // Authorization: Bearer <token>
fromHeader('x-auth', '') // X-Auth: <token>
fromCookie('access_token') // Cookie: access_token=<...>
fromQuery('token') // ?token=<...>
Each extractor returns (ctx: RequestContext) => string | null. Use them inside your provider's authenticate() method - they are not coupled to JWT, you can use them for API keys, session cookies, anything header- or cookie-based.
timingSafeEqual
Constant-time string comparison - use it when comparing pre-hashed tokens, HMAC digests, or API keys to avoid leaking length/position information through timing side channels.
import { timingSafeEqual } from '@miiajs/auth'
if (!timingSafeEqual(providedHash, expectedHash)) {
throw new UnauthorizedException()
}
Not for plaintext passwords - use bcrypt.compare or argon2.verify instead.
Recipes
Complete worked examples - standalone, copy-pasteable patterns. Each recipe demonstrates the AuthProvider interface for a different credential source:
- JWT Provider - verify Bearer tokens
- Local Provider - username/password login with argon2
- OAuth2 Provider - GitHub login flow
Testing
Override the AuthProvider at the DI level with TestApp.override(). Guards run unchanged - they resolve whichever provider you wired up, so a stub provider is enough to drive any auth path:
import { Injectable } from '@miiajs/core'
import { TestApp } from '@miiajs/core/testing'
import type { AuthProvider } from '@miiajs/auth'
import { JwtProvider } from './jwt.provider.js'
@Injectable()
class StubProvider implements AuthProvider {
authenticate() {
return { id: 1, email: 'alice@example.com' }
}
}
const app = await TestApp.create(AppModule)
.override(JwtProvider, StubProvider)
.compile()
const res = await app.request('GET', '/me')
expect(res.status).toBe(200)
await app.close()
To exercise 401/403 paths, override with a provider that throws UnauthorizedException or ForbiddenException from @miiajs/core. The guard rethrows them unchanged.
Exports
import {
AuthGuard,
fromHeader,
fromCookie,
fromQuery,
timingSafeEqual,
type AuthProvider,
type TokenExtractor,
} from '@miiajs/auth'