beta

Overview

Lightweight auth primitives - AuthProvider interface, AuthGuard factory, HTTP token extractors, timing-safe utilities.

@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

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:

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'