| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- // @vitest-environment node
- import { describe, it, expect, beforeAll } from "vitest";
- import { SignJWT, jwtVerify, decodeProtectedHeader, decodeJwt } from "jose";
- import { mintAccessToken, verifyAccessToken } from "@/lib/auth/jwt";
- const SECRET = "test-jwt-secret-with-at-least-32-characters-of-length-yes";
- beforeAll(() => {
- process.env.JWT_SECRET = SECRET;
- });
- const sub = "00000000-0000-0000-0000-000000000abc";
- const session_id = "11111111-1111-1111-1111-111111111111";
- describe("mintAccessToken / verifyAccessToken", () => {
- it("roundtrips and preserves iat_original", async () => {
- const iat_original = Math.floor(Date.now() / 1000) - 60;
- const token = await mintAccessToken({ sub, session_id, iat_original });
- const claims = await verifyAccessToken(token);
- expect(claims).not.toBeNull();
- expect(claims!.sub).toBe(sub);
- expect(claims!.session_id).toBe(session_id);
- expect(claims!.iat_original).toBe(iat_original);
- });
- it("sets the protected header (alg=HS256, typ=JWT, kid=v1)", async () => {
- const token = await mintAccessToken({
- sub,
- session_id,
- iat_original: Math.floor(Date.now() / 1000),
- });
- const header = decodeProtectedHeader(token);
- expect(header.alg).toBe("HS256");
- expect(header.typ).toBe("JWT");
- expect(header.kid).toBe("v1");
- });
- it("sets iss=moviedice and aud=authenticated", async () => {
- const token = await mintAccessToken({
- sub,
- session_id,
- iat_original: Math.floor(Date.now() / 1000),
- });
- const payload = decodeJwt(token);
- expect(payload.iss).toBe("moviedice");
- expect(payload.aud).toBe("authenticated");
- expect(payload.role).toBe("authenticated");
- expect(payload.is_anonymous).toBe(true);
- });
- it("rejects an expired token", async () => {
- const secret = new TextEncoder().encode(SECRET);
- const past = Math.floor(Date.now() / 1000) - 7200; // 2h ago
- const token = await new SignJWT({
- sub,
- session_id,
- iat_original: past,
- role: "authenticated",
- })
- .setProtectedHeader({ alg: "HS256", typ: "JWT", kid: "v1" })
- .setIssuedAt(past)
- .setNotBefore(past - 10)
- .setExpirationTime(past + 3600) // expired 1h ago
- .setIssuer("moviedice")
- .setAudience("authenticated")
- .sign(secret);
- expect(await verifyAccessToken(token)).toBeNull();
- });
- it("rejects a token whose nbf is in the future (beyond skew)", async () => {
- const secret = new TextEncoder().encode(SECRET);
- const future = Math.floor(Date.now() / 1000) + 600; // +10min
- const token = await new SignJWT({
- sub,
- session_id,
- iat_original: future,
- role: "authenticated",
- })
- .setProtectedHeader({ alg: "HS256", typ: "JWT", kid: "v1" })
- .setIssuedAt(future)
- .setNotBefore(future)
- .setExpirationTime(future + 3600)
- .setIssuer("moviedice")
- .setAudience("authenticated")
- .sign(secret);
- expect(await verifyAccessToken(token)).toBeNull();
- });
- it("rejects a token signed with the wrong secret", async () => {
- const wrong = new TextEncoder().encode("wrong-secret-also-32-characters-long-yes-yes!");
- const now = Math.floor(Date.now() / 1000);
- const token = await new SignJWT({
- sub,
- session_id,
- iat_original: now,
- role: "authenticated",
- })
- .setProtectedHeader({ alg: "HS256", typ: "JWT", kid: "v1" })
- .setIssuedAt()
- .setNotBefore("-10s")
- .setExpirationTime("1h")
- .setIssuer("moviedice")
- .setAudience("authenticated")
- .sign(wrong);
- expect(await verifyAccessToken(token)).toBeNull();
- });
- it("rejects a token with wrong issuer or audience", async () => {
- const secret = new TextEncoder().encode(SECRET);
- const now = Math.floor(Date.now() / 1000);
- const wrongIss = await new SignJWT({ sub, session_id, iat_original: now })
- .setProtectedHeader({ alg: "HS256", typ: "JWT", kid: "v1" })
- .setIssuedAt()
- .setNotBefore("-10s")
- .setExpirationTime("1h")
- .setIssuer("not-moviedice")
- .setAudience("authenticated")
- .sign(secret);
- expect(await verifyAccessToken(wrongIss)).toBeNull();
- const wrongAud = await new SignJWT({ sub, session_id, iat_original: now })
- .setProtectedHeader({ alg: "HS256", typ: "JWT", kid: "v1" })
- .setIssuedAt()
- .setNotBefore("-10s")
- .setExpirationTime("1h")
- .setIssuer("moviedice")
- .setAudience("not-authenticated")
- .sign(secret);
- expect(await verifyAccessToken(wrongAud)).toBeNull();
- });
- it("verifies a token signed seconds in the future thanks to nbf skew", async () => {
- // mintAccessToken sets nbf = -10s; jose's jwtVerify uses clockTolerance:0
- // by default but our nbf is already 10s in the past, so it should verify
- // immediately.
- const token = await mintAccessToken({
- sub,
- session_id,
- iat_original: Math.floor(Date.now() / 1000),
- });
- const claims = await verifyAccessToken(token);
- expect(claims).not.toBeNull();
- });
- });
|