fix(auth): bound OIDC initiation and transport
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import Fastify from "fastify";
|
||||
import cookie from "@fastify/cookie";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { registerAuthRoutes } from "../src/auth/routes.js";
|
||||
import type { LoadedAuthConfig, OidcStateRecord } from "../src/auth/types.js";
|
||||
import type { AuthSessionStore } from "../src/auth/session-store.js";
|
||||
@@ -76,8 +76,10 @@ function fixture(options: {
|
||||
const creates: Array<Record<string, unknown>> = [];
|
||||
const createTimes: Array<Date | undefined> = [];
|
||||
const callbacks: URL[] = [];
|
||||
let authorizationRequests = 0;
|
||||
const protocol: OidcProtocol = {
|
||||
authorizationUrl: async ({ state: received, nonce: receivedNonce, codeVerifier }) => {
|
||||
authorizationRequests += 1;
|
||||
expect(received).toBe(state);
|
||||
expect(receivedNonce).toHaveLength(43);
|
||||
expect(codeVerifier).toHaveLength(43);
|
||||
@@ -134,6 +136,7 @@ function fixture(options: {
|
||||
createdApps.push(app);
|
||||
return {
|
||||
app, creates, createTimes, callbacks, stateInputs,
|
||||
authorizationRequests: () => authorizationRequests,
|
||||
setConfig(next: LoadedAuthConfig) { loaded = next; },
|
||||
setProtocolAvailable(available: boolean) { protocolAvailable = available; },
|
||||
stateWasConsumed: () => storedState === undefined,
|
||||
@@ -161,6 +164,42 @@ async function finishOidcLogin(
|
||||
});
|
||||
}
|
||||
|
||||
test("rate limits OIDC initiation before state creation and provider discovery", async () => {
|
||||
const subject = fixture();
|
||||
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
expect((await beginOidcLogin(subject)).response.statusCode).toBe(302);
|
||||
}
|
||||
const limited = await beginOidcLogin(subject);
|
||||
|
||||
expect(limited.response.statusCode).toBe(429);
|
||||
expect(limited.response.json()).toEqual({
|
||||
code: "login_rate_limited",
|
||||
error: "Too many login attempts",
|
||||
});
|
||||
expect(subject.stateInputs).toHaveLength(20);
|
||||
expect(subject.authorizationRequests()).toBe(20);
|
||||
});
|
||||
|
||||
test("OIDC initiation rate-limit capacity expires after ten minutes", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2030-01-02T03:04:05.000Z"));
|
||||
const subject = fixture();
|
||||
try {
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
expect((await beginOidcLogin(subject)).response.statusCode).toBe(302);
|
||||
}
|
||||
expect((await beginOidcLogin(subject)).response.statusCode).toBe(429);
|
||||
|
||||
vi.advanceTimersByTime(10 * 60_000 + 1);
|
||||
|
||||
expect((await beginOidcLogin(subject)).response.statusCode).toBe(302);
|
||||
expect(subject.authorizationRequests()).toBe(21);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
test("creates digest-only bound state, maps exact groups, creates a cookie session, and redirects safely", async () => {
|
||||
const subject = fixture();
|
||||
const { response: start, cookie } = await beginOidcLogin(subject);
|
||||
@@ -298,6 +337,20 @@ test.each([
|
||||
expect(subject.callbacks).toEqual([]);
|
||||
});
|
||||
|
||||
test("boundedly burns a canonical state from an oversized callback URL", async () => {
|
||||
const subject = fixture();
|
||||
const { cookie } = await beginOidcLogin(subject);
|
||||
const oversized = `state=${state}&code=good&padding=${"x".repeat(4096)}`;
|
||||
|
||||
const failed = await finishOidcLogin(subject, cookie, oversized);
|
||||
|
||||
expect(failed.statusCode).toBe(401);
|
||||
expectTransactionCleared(failed);
|
||||
expect(subject.stateWasConsumed()).toBe(true);
|
||||
expect(subject.callbacks).toEqual([]);
|
||||
expect((await finishOidcLogin(subject, cookie)).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test("rejects an empty direct groups claim without creating a session cookie", async () => {
|
||||
const subject = fixture({ identity: {
|
||||
issuer, subject: "user-123", groups: [], tokenExpiresAt: new Date(Date.now() + 60_000),
|
||||
|
||||
Reference in New Issue
Block a user