feat: 实现演示验证码和令牌轮换

This commit is contained in:
root
2026-09-14 18:43:45 +08:00
parent 6b901e074e
commit 3e04197b02
24 changed files with 1486 additions and 12 deletions
+82
View File
@@ -0,0 +1,82 @@
import { HttpStatus, Injectable } from "@nestjs/common";
import { ErrorCode } from "@drift/contracts";
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { DomainException } from "../common/domain.exception.js";
import { envInt, jwtSecret } from "./auth.config.js";
export interface AccessClaims {
sub: string;
session_id: string;
device_id: string;
scopes: string[];
token_version: number;
iss: string;
aud: string;
exp: number;
iat: number;
}
const b64 = (v: Buffer | string) => Buffer.from(v).toString("base64url");
@Injectable()
export class TokenService {
issueAccess(
input: Omit<AccessClaims, "iss" | "aud" | "exp" | "iat">,
): string {
const now = Math.floor(Date.now() / 1000);
const header = b64(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const body = b64(
JSON.stringify({
...input,
iss: process.env.JWT_ISSUER ?? "drift-api",
aud: process.env.JWT_AUDIENCE ?? "drift-web",
iat: now,
exp: now + envInt("ACCESS_TOKEN_TTL_SECONDS", 900),
}),
);
const data = `${header}.${body}`;
return `${data}.${createHmac("sha256", jwtSecret()).update(data).digest("base64url")}`;
}
verifyAccess(token: string): AccessClaims {
try {
const parts = token.split(".");
if (parts.length !== 3) throw new Error();
const [h, p, s] = parts as [string, string, string];
const header = JSON.parse(Buffer.from(h, "base64url").toString()) as {
alg?: string;
};
if (header.alg !== "HS256") throw new Error();
const expected = createHmac("sha256", jwtSecret())
.update(`${h}.${p}`)
.digest();
const actual = Buffer.from(s, "base64url");
if (
expected.length !== actual.length ||
!timingSafeEqual(expected, actual)
)
throw new Error();
const claims = JSON.parse(
Buffer.from(p, "base64url").toString(),
) as AccessClaims;
if (
claims.iss !== (process.env.JWT_ISSUER ?? "drift-api") ||
claims.aud !== (process.env.JWT_AUDIENCE ?? "drift-web")
)
throw new Error();
if (claims.exp <= Math.floor(Date.now() / 1000))
throw new DomainException(
ErrorCode.AUTH_TOKEN_EXPIRED,
"Access token expired",
HttpStatus.UNAUTHORIZED,
);
return claims;
} catch (error) {
if (error instanceof DomainException) throw error;
throw new DomainException(
ErrorCode.AUTH_UNAUTHORIZED,
"Unauthorized",
HttpStatus.UNAUTHORIZED,
);
}
}
newRefresh(sessionId: string): string {
return `${sessionId}.${randomBytes(32).toString("base64url")}`;
}
}