JWT Authentication
@zeltjs/auth-jwt は、SPA・モバイルアプリ・APIのためのステートレスなJWTベース認証を提供します。
インストール
pnpm add @zeltjs/auth-jwt
クイックスタート
1. secretを設定する
JWT_SECRET 環境変数を設定します。
# .env
JWT_SECRET=your-secret-key-at-least-32-characters
2. middlewareを登録する
const app = createApp([http({
controllers: [AuthController, UserController],
middlewares: [JwtMiddleware],
})], { configs: [JwtConfig] });
3. トークンを発行する
ログイン時、JwtService を使ってトークンに署名します。
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.string(),
});
@Controller('/auth')
class AuthController {
constructor(private jwtService = inject(JwtService)) {}
@Post('/login')
async login(req = request(LoginSchema)) {
const body = await req.body();
const user = await validateCredentials(body.email, body.password);
if (!user) {
throw new HTTPException(401, { message: 'Invalid credentials' });
}
const token = await this.jwtService.sign({
sub: user.id,
roles: user.roles,
});
return { token };
}
}
4. ルートを保護する
認証を必須にするには @Authorized() を使います。
@Controller('/users')
class UserController {
@Authorized()
@Get('/me')
me(user = currentUser()) {
return user;
}
}
JwtService API
| メソッド | 説明 |
|---|---|
sign(payload) | 署名付きJWTトークンを作成する |
verify(token) | トークンを検証してデコードする(無効な場合は例外を投げる) |
decode(token) | 検証なしでデコードする(エラー時は null を返す) |
Sign
カスタムpayloadで署名付きトークンを作成します。
async createToken(userId: string) {
return this.jwtService.sign({
sub: userId,
roles: ['admin', 'user'],
customClaim: 'value',
});
}
}
Verify
トークンを検証し、payloadを取得します(無効または期限切れの場合は例外を投げます)。
async validateToken(token: string) {
try {
const payload = await this.jwtService.verify(token);
console.log(payload.sub);
return payload;
} catch {
return null;
}
}
}
Decode
検証なしでデコードします(期限切れのトークンを読む際に便利です)。
readToken(token: string) {
const payload = this.jwtService.decode(token);
if (payload) {
console.log(payload.sub);
}
return payload;
}
}
設定
JwtConfig を継承して動作をカスタマイズします。
@Config
class CustomJwtConfig extends JwtConfig {
constructor(private userRepo = inject(UserRepository)) {
super();
}
override get secret(): string {
return this.env.getRequired('JWT_SECRET');
}
override get expiresIn(): string {
return '7d';
}
override get resolveUser(): (payload: JwtPayload) => Promise<ResolveUserResult> {
return async (payload) => {
const user = await this.userRepo.findById(payload.sub!);
return {
user: { id: user.id, name: user.name, email: user.email },
roles: user.roles,
};
};
}
}
カスタムconfigを登録します。
const app = createApp([http({
controllers: [AuthController, UserController],
middlewares: [JwtMiddleware],
})], { configs: [CustomJwtConfig] });
Configuration Options
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
secret | string | env.getRequired('JWT_SECRET') | 署名用のsecret key |
expiresIn | string | '1h' | トークンの有効期限(例: '15m'、'7d') |
resolveUser | function | { user: sub, roles: [] } を返す | JWTのpayloadからユーザーを解決する |
クライアント側の統合
トークンの送信
クライアントは Authorization ヘッダーにトークンを含める必要があります。
fetch('/api/users/me', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
トークンの保存
クライアント上ではトークンを安全に保存してください。
| プラットフォーム | 推奨される保存先 |
|---|---|
| ブラウザSPA | httpOnly cookieまたはメモリ(localStorage は避ける) |
| モバイルアプリ | セキュアストレージ(Keychain / Keystore) |
| サーバー間通信 | 環境変数 |
トークンのリフレッシュパターン
長期間のセッションでは、refresh tokenのフローを実装します。
@Controller('/auth')
class AuthController {
constructor(
private jwtService = inject(JwtService),
private userRepo = inject(UserRepository)
) {}
@Post('/refresh')
async refresh(req = request(RefreshSchema)) {
const body = await req.body();
const payload = await this.jwtService.verify(body.refreshToken);
const user = await this.userRepo.findById(payload.sub!);
const accessToken = await this.jwtService.sign({
sub: user.id,
roles: user.roles,
});
return { accessToken };
}
}
エラーレスポンス
| ステータス | コード | 発生条件 |
|---|---|---|
| 401 | UNAUTHORIZED | トークンがない、無効、または期限切れ |
| 403 | FORBIDDEN | トークンは有効だが必要なroleがない |
{
"code": "UNAUTHORIZED",
"message": "Authentication required"
}
Edge Runtimeのサポート
@zeltjs/auth-jwt はWeb Crypto APIをサポートする jose ライブラリを使用しており、以下と互換性があります。
- Cloudflare Workers
- Vercel Edge Functions
- Deno Deploy
- Node.js