JWT
@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
npm install @miiajs/jwt jose
pnpm add @miiajs/jwt jose
yarn 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
| Field | Type | Description |
|---|---|---|
secret | string | HMAC secret (required for HS algorithms) |
publicKey | string | CryptoKey | Public key for RS/ES/EdDSA verification |
privateKey | string | CryptoKey | Private key for RS/ES/EdDSA signing |
algorithm | string | Default algorithm (HS256, RS256, ...) |
expiresIn | string | number | Default expiration (e.g. '1h', 3600) |
issuer | string | Default iss claim |
audience | string | Default 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'