64 lines
1.6 KiB
TypeScript
64 lines
1.6 KiB
TypeScript
import crypto from 'node:crypto';
|
|
|
|
import { jwtVerify, SignJWT } from 'jose';
|
|
|
|
import type { AppConfig } from '../config.js';
|
|
import { unauthorized } from '../errors.js';
|
|
|
|
if (!globalThis.crypto?.subtle) {
|
|
Object.assign(globalThis, {
|
|
crypto: crypto.webcrypto,
|
|
});
|
|
}
|
|
|
|
if (typeof globalThis.structuredClone !== 'function') {
|
|
Object.assign(globalThis, {
|
|
structuredClone<T>(value: T) {
|
|
return JSON.parse(JSON.stringify(value)) as T;
|
|
},
|
|
});
|
|
}
|
|
|
|
export type AccessTokenClaims = {
|
|
userId: string;
|
|
tokenVersion: number;
|
|
};
|
|
|
|
function getSecret(config: AppConfig) {
|
|
return new TextEncoder().encode(config.jwtSecret);
|
|
}
|
|
|
|
export async function signAccessToken(
|
|
claims: AccessTokenClaims,
|
|
config: AppConfig,
|
|
) {
|
|
return new SignJWT({ ver: claims.tokenVersion })
|
|
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
|
|
.setSubject(claims.userId)
|
|
.setIssuer(config.jwtIssuer)
|
|
.setIssuedAt()
|
|
.setExpirationTime(config.jwtExpiresIn)
|
|
.sign(getSecret(config));
|
|
}
|
|
|
|
export async function verifyAccessToken(token: string, config: AppConfig) {
|
|
try {
|
|
const { payload } = await jwtVerify(token, getSecret(config), {
|
|
issuer: config.jwtIssuer,
|
|
});
|
|
const userId = typeof payload.sub === 'string' ? payload.sub : '';
|
|
const tokenVersion = typeof payload.ver === 'number' ? payload.ver : NaN;
|
|
|
|
if (!userId || !Number.isFinite(tokenVersion)) {
|
|
throw unauthorized('JWT 内容无效');
|
|
}
|
|
|
|
return {
|
|
userId,
|
|
tokenVersion,
|
|
} satisfies AccessTokenClaims;
|
|
} catch (error) {
|
|
throw unauthorized('JWT 校验失败');
|
|
}
|
|
}
|