beta

JWT

Injectable JwtService for signing and verifying JSON Web Tokens - a thin wrapper around jose.

@miiajs/jwt is a tiny, injectable JWT service for MiiaJS. It wraps jose and exposes a single JwtService with sign(), verify(), and a configurable JwtModule.

It intentionally does not provide auth strategies, guards, or middleware - those live in @miiajs/auth, with a ready-made JWT Provider recipe to build on top of.

Installation

bun add @miiajs/jwt jose

jose is a required peer dependency.

Configuration

import { Module } from '@miiajs/core'
import { JwtModule } from '@miiajs/jwt'

@Module({
  imports: [
    JwtModule.configure({
      secret: process.env.JWT_SECRET!,
      algorithm: 'HS256',
      expiresIn: '1h',
    }),
  ],
})
class AppModule {}

With a factory function for accessing other services from the container:

import { ConfigService } from '@miiajs/config'

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

Options

FieldTypeDescription
secretstringHMAC secret (required for HS algorithms)
publicKeystring | CryptoKeyPublic key for RS/ES/EdDSA verification
privateKeystring | CryptoKeyPrivate key for RS/ES/EdDSA signing
algorithmstringDefault algorithm (HS256, RS256, ...)
expiresInstring | numberDefault expiration (e.g. '1h', 3600)
issuerstringDefault iss claim
audiencestringDefault aud claim

JwtService

import { Injectable, inject } from '@miiajs/core'
import { JwtService } from '@miiajs/jwt'

@Injectable()
export class AuthService {
  private jwtService = inject(JwtService)

  async issueToken(user: { id: number; email: string }) {
    return this.jwtService.sign({ sub: user.id, email: user.email })
  }

  async readToken(token: string) {
    return this.jwtService.verify<{ sub: number; email: string }>(token)
  }
}

sign(payload, options?)

Signs a JWT and returns it as a compact string. Per-call options override module defaults.

sign(payload: JwtPayload, options?: JwtSignOptions): Promise<string>
await jwt.sign({ sub: userId }, {
  expiresIn: '7d',     // override module default
  algorithm: 'HS384',  // override module default
  issuer: 'my-app',
  audience: 'my-api',
  subject: String(userId),
  notBefore: '30s',
})

verify(token, options?)

Verifies and decodes a JWT. Rejects if the token is expired, the signature is invalid, or the algorithm is outside the whitelist.

verify<T>(token: string, options?: JwtVerifyOptions): Promise<T>
const payload = await jwt.verify<{ sub: number }>(token, {
  algorithms: ['HS256'],   // override whitelist
  issuer: 'my-app',
  audience: 'my-api',
})

Algorithm safety

verify() enforces an algorithm whitelist. By default, only the module's configured algorithm (or HS256 for symmetric keys / RS256 for asymmetric) is accepted. Tokens signed with a different algorithm are rejected - this prevents algorithm confusion attacks where an attacker swaps RS256 for HS256.

Override the whitelist explicitly when you need to accept multiple algorithms:

await jwt.verify(token, { algorithms: ['RS256', 'ES256'] })

Beyond auth

The same service powers non-HTTP use cases - anywhere you need a self-contained, signed, time-bounded token:

@Injectable()
class EmailTokens {
  private jwtService = inject(JwtService)

  async sendVerification(email: string) {
    const token = await this.jwtService.sign(
      { email, purpose: 'verify-email' },
      { expiresIn: '24h' },
    )
    // include `token` in the verification link sent to the user
  }
}

Common patterns: email verification links, password reset tokens, signed download URLs, CSRF double-submit cookies, webhook signatures.

Standalone usage

For scripts and workers that don't run inside a MiiaJS container, construct the service directly with options:

import { JwtService } from '@miiajs/jwt'

const jwt = new JwtService({
  secret: process.env.JWT_SECRET!,
  expiresIn: '1h',
})

const token = await jwt.sign({ sub: 'cron-bot' })

When constructed without options, JwtService resolves JWT_OPTIONS from the active container - that's the normal DI path.

Testing

Use TestApp to resolve the service:

const app = await TestApp.create(AppModule).compile()
const jwt = app.resolve(JwtService)
const token = await jwt.sign({ sub: '1' })

const res = await app.request('GET', '/api/me', {
  headers: { authorization: `Bearer ${token}` },
})

Exports

import {
  JwtModule,
  JwtService,
  JWT_OPTIONS,
  type JwtOptions,
  type JwtPayload,
  type JwtSignOptions,
  type JwtVerifyOptions,
} from '@miiajs/jwt'