fix(auth): persist signup verification sessions and supply delivery URLs
This commit is contained in:
1 parent
84e16ebc41
commit
f7fe3bb17b
12 files changed
+237
-8
No files matched your search
@@ -15,7 +15,7 @@ function context(request: Request): Context {
|
||||
t: (key: string) => key,
|
||||
ip: "127.0.0.1",
|
||||
user: null,
|
||||
cookies: {} as Context["cookies"],
|
||||
cookies: { get: () => undefined } as unknown as Context["cookies"],
|
||||
localStorage: {} as Context["localStorage"],
|
||||
session: {
|
||||
id: () => "http-test-session",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createContext } from "@wrnexus/core";
|
||||
import { createAuthEngine } from "../src/engine.ts";
|
||||
import { createAuthHttpHandlers } from "../src/http/index.ts";
|
||||
import { MemoryAuthStore } from "../src/stores/memory.ts";
|
||||
import { getVerificationUser, VERIFICATION_SESSION_COOKIE } from "../src/verification-session.ts";
|
||||
import type { AuthDeliveryMessage } from "../src/types.ts";
|
||||
|
||||
test("onSignUp issues a persistent verification-only cookie and registration receives a trusted link origin", async () => {
|
||||
const store = new MemoryAuthStore();
|
||||
const messages: AuthDeliveryMessage[] = [];
|
||||
let hookCalls = 0;
|
||||
const options = {
|
||||
store,
|
||||
secret: "signup-session-test-secret-longer-than-32-characters",
|
||||
requireVerifiedEmail: true,
|
||||
onSignUp() {
|
||||
hookCalls++;
|
||||
return { verificationSession: true, redirectTo: "/verify-email" };
|
||||
},
|
||||
delivery: {
|
||||
async send(message: AuthDeliveryMessage) {
|
||||
messages.push(message);
|
||||
},
|
||||
},
|
||||
};
|
||||
const engine = createAuthEngine(options);
|
||||
const url = new URL("https://sso.example.test/api/auth/register");
|
||||
const ctx = createContext(
|
||||
new Request(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
displayName: "Signup Test",
|
||||
email: "signup@example.test",
|
||||
password: "StrongPassword123!",
|
||||
consent: true,
|
||||
baseUrl: "https://attacker.example",
|
||||
}),
|
||||
}),
|
||||
url,
|
||||
);
|
||||
const response = await createAuthHttpHandlers({ engine }).register(ctx);
|
||||
expect(response.status).toBe(303);
|
||||
expect(response.headers.get("location")).toBe("https://sso.example.test/verify-email");
|
||||
expect(hookCalls).toBe(1);
|
||||
expect(messages[0]!.url).toBe(
|
||||
`https://sso.example.test/verify-email?token=${messages[0]!.token}`,
|
||||
);
|
||||
expect(messages[0]!.url).not.toContain("attacker");
|
||||
const setCookie = ctx.cookies
|
||||
.headers()
|
||||
.filter((value) => value.startsWith(`${VERIFICATION_SESSION_COOKIE}=`))
|
||||
.at(-1)!;
|
||||
expect(setCookie).toContain("HttpOnly");
|
||||
expect(setCookie).toContain("Secure");
|
||||
expect(setCookie).toContain("SameSite=Lax");
|
||||
const cookie = setCookie.split(";")[0]!;
|
||||
const restartedEngine = createAuthEngine(options);
|
||||
const nextUrl = new URL("https://sso.example.test/api/auth/verification/request");
|
||||
const next = createContext(
|
||||
new Request(nextUrl, {
|
||||
method: "POST",
|
||||
headers: { cookie, "content-type": "application/json" },
|
||||
body: JSON.stringify({ type: "email", identifier: "attacker@example.test" }),
|
||||
}),
|
||||
nextUrl,
|
||||
);
|
||||
const pending = await getVerificationUser(next, restartedEngine);
|
||||
expect(pending?.id).toBe(messages[0]!.user.id);
|
||||
expect(next.user).toBeUndefined();
|
||||
expect(
|
||||
(await createAuthHttpHandlers({ engine: restartedEngine }).requestVerification(next)).status,
|
||||
).toBe(200);
|
||||
expect(messages[1]!.destination).toBe("signup@example.test");
|
||||
const token = decodeURIComponent(cookie.slice(cookie.indexOf("=") + 1));
|
||||
expect(await restartedEngine.validateSession(token)).toBeUndefined();
|
||||
expect((await restartedEngine.verifyEmail(token)).ok).toBe(false);
|
||||
expect((await restartedEngine.verifyEmail(messages[1]!.token!)).ok).toBe(true);
|
||||
expect(await getVerificationUser(next, restartedEngine)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("verification sessions reject expiry, tampering, wrong purpose, and revoked tokens", async () => {
|
||||
let time = 100_000;
|
||||
const engine = createAuthEngine({
|
||||
store: new MemoryAuthStore(),
|
||||
secret: "signup-session-test-secret-longer-than-32-characters",
|
||||
clock: { now: () => time },
|
||||
});
|
||||
const user = (
|
||||
await engine.register({ email: "expiry@example.test", password: "StrongPassword123!" })
|
||||
).user!;
|
||||
const token = await engine.createVerificationSession(user.id);
|
||||
expect(await engine.validateVerificationSession(`${token}x`)).toBeUndefined();
|
||||
expect(
|
||||
await engine.validateVerificationSession(await engine.issueToken(user.id, "verify-email")),
|
||||
).toBeUndefined();
|
||||
time += 30 * 60_000 + 1;
|
||||
expect(await engine.validateVerificationSession(token)).toBeUndefined();
|
||||
const renewed = await engine.createVerificationSession(user.id);
|
||||
await engine.consumeToken(renewed, "verification-session");
|
||||
expect(await engine.validateVerificationSession(renewed)).toBeUndefined();
|
||||
});
|
||||
Reference in new issue
Block a user