first commit
This commit is contained in:
141
packages/auth/src/tokens.test.ts
Normal file
141
packages/auth/src/tokens.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
generateToken,
|
||||
hashToken,
|
||||
generateTokenWithHash,
|
||||
generateSessionId,
|
||||
generateAuthSecret,
|
||||
secureCompare,
|
||||
computeS256Challenge,
|
||||
encrypt,
|
||||
decrypt,
|
||||
} from "./tokens.js";
|
||||
|
||||
const BASE64URL_REGEX = /^[A-Za-z0-9_-]+$/;
|
||||
const NO_PADDING_REGEX = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
describe("tokens", () => {
|
||||
describe("generateToken", () => {
|
||||
it("generates a base64url-encoded token", () => {
|
||||
const token = generateToken();
|
||||
expect(token).toMatch(BASE64URL_REGEX);
|
||||
// 32 bytes = 43 base64url characters (without padding)
|
||||
expect(token.length).toBe(43);
|
||||
});
|
||||
|
||||
it("generates unique tokens", () => {
|
||||
// eslint-disable-next-line e18e/prefer-array-fill -- We need unique tokens, not the same token repeated
|
||||
const tokens = new Set(Array.from({ length: 100 }, () => generateToken()));
|
||||
expect(tokens.size).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashToken", () => {
|
||||
it("produces consistent hashes", () => {
|
||||
const token = generateToken();
|
||||
const hash1 = hashToken(token);
|
||||
const hash2 = hashToken(token);
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
|
||||
it("produces different hashes for different tokens", () => {
|
||||
const token1 = generateToken();
|
||||
const token2 = generateToken();
|
||||
expect(hashToken(token1)).not.toBe(hashToken(token2));
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateTokenWithHash", () => {
|
||||
it("returns both token and hash", () => {
|
||||
const { token, hash } = generateTokenWithHash();
|
||||
expect(token).toBeDefined();
|
||||
expect(hash).toBeDefined();
|
||||
expect(hashToken(token)).toBe(hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateSessionId", () => {
|
||||
it("generates a shorter session ID", () => {
|
||||
const sessionId = generateSessionId();
|
||||
expect(sessionId).toMatch(BASE64URL_REGEX);
|
||||
// 20 bytes = 27 base64url characters
|
||||
expect(sessionId.length).toBe(27);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateAuthSecret", () => {
|
||||
it("generates a 32-byte secret", () => {
|
||||
const secret = generateAuthSecret();
|
||||
expect(secret).toMatch(BASE64URL_REGEX);
|
||||
expect(secret.length).toBe(43);
|
||||
});
|
||||
});
|
||||
|
||||
describe("secureCompare", () => {
|
||||
it("returns true for equal strings", () => {
|
||||
expect(secureCompare("hello", "hello")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for different strings", () => {
|
||||
expect(secureCompare("hello", "world")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for different length strings", () => {
|
||||
expect(secureCompare("hello", "hello!")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeS256Challenge", () => {
|
||||
it("produces correct S256 challenge for a known verifier", () => {
|
||||
// RFC 7636 Appendix B test vector:
|
||||
// verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||
// expected challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
|
||||
const challenge = computeS256Challenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
|
||||
expect(challenge).toBe("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
|
||||
});
|
||||
|
||||
it("produces base64url output without padding", () => {
|
||||
const challenge = computeS256Challenge("test-verifier-string");
|
||||
expect(challenge).toMatch(NO_PADDING_REGEX);
|
||||
expect(challenge).not.toContain("=");
|
||||
});
|
||||
|
||||
it("is deterministic", () => {
|
||||
const a = computeS256Challenge("same-input");
|
||||
const b = computeS256Challenge("same-input");
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("produces different output for different input", () => {
|
||||
const a = computeS256Challenge("verifier-one");
|
||||
const b = computeS256Challenge("verifier-two");
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encrypt/decrypt", () => {
|
||||
const secret = generateAuthSecret();
|
||||
|
||||
it("encrypts and decrypts a string", async () => {
|
||||
const plaintext = "my-secret-value";
|
||||
const encrypted = await encrypt(plaintext, secret);
|
||||
const decrypted = await decrypt(encrypted, secret);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("produces different ciphertext each time (due to random IV)", async () => {
|
||||
const plaintext = "my-secret-value";
|
||||
const encrypted1 = await encrypt(plaintext, secret);
|
||||
const encrypted2 = await encrypt(plaintext, secret);
|
||||
expect(encrypted1).not.toBe(encrypted2);
|
||||
});
|
||||
|
||||
it("fails to decrypt with wrong secret", async () => {
|
||||
const plaintext = "my-secret-value";
|
||||
const encrypted = await encrypt(plaintext, secret);
|
||||
const wrongSecret = generateAuthSecret();
|
||||
await expect(decrypt(encrypted, wrongSecret)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user