feat: 实现演示验证码和令牌轮换
This commit is contained in:
@@ -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")}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user