Skip to content

Commit 085cdbf

Browse files
committed
feat(auth): add session ttl reuse
1 parent cfab7c6 commit 085cdbf

14 files changed

Lines changed: 97 additions & 33 deletions

File tree

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ LOGTO_CLIENT_SECRET=o4lvhz8xT5W6nKgAFOBNTqjArJYPcWYx
1212

1313
MOCKAUTH_KEY_ENCRYPTION_SECRET=abcdefghijklmnopqrstuvwxyz123456
1414
MOCKAUTH_ALLOW_ANY_REDIRECT=false
15+
MOCKAUTH_SESSION_TTL_SECONDS=2592000

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ LOGTO_CLIENT_SECRET=replace-with-logto-client-secret
1212
MOCKAUTH_KEY_ENCRYPTION_SECRET=replace-with-32-character-secret
1313
MOCKAUTH_ALLOW_ANY_REDIRECT=false
1414
MOCKAUTH_ALLOW_INSECURE_TEST_COOKIE=false
15+
MOCKAUTH_SESSION_TTL_SECONDS=2592000
1516

1617
AUDIT_LOG_RETENTION_DAYS=90
1718
CRON_SECRET=replace-with-cron-secret

.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ MOCKAUTH_KEY_ENCRYPTION_SECRET=abcdefghijklmnopqrstuvwxyz123456
1313
MOCKAUTH_ALLOW_ANY_REDIRECT=false
1414
ENABLE_TEST_ROUTES=true
1515
MOCKAUTH_ALLOW_INSECURE_TEST_COOKIE=true
16+
MOCKAUTH_SESSION_TTL_SECONDS=2592000
1617
PLAYWRIGHT_LD_LIBRARY_PATH=../playwright-libs/sysroot/usr/lib/x86_64-linux-gnu:../playwright-libs/sysroot/usr/lib/x86_64-linux-gnu/gbm
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const navigateTo = (url: string) => {
2+
window.location.assign(url);
3+
};

src/app/admin/clients/[clientId]/test/test-run-again-button.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"use client";
22

33
import { useTransition } from "react";
4-
import { useRouter } from "next/navigation";
54
import type { ReactNode } from "react";
65

76
import { prepareClientOauthTestAction } from "@/app/admin/actions";
87
import { Button, type ButtonProps } from "@/components/ui/button";
98
import { useToast } from "@/components/ui/use-toast";
9+
import { navigateTo } from "@/app/admin/clients/[clientId]/test/navigation";
1010

1111
type Props = {
1212
clientId: string;
@@ -20,7 +20,6 @@ type Props = {
2020

2121
export function TestRunAgainButton({ clientId, scopes, redirectUri, variant, size, testId, children }: Props) {
2222
const [pending, startTransition] = useTransition();
23-
const router = useRouter();
2423
const { toast } = useToast();
2524
const label = children ?? "Run again";
2625

@@ -36,7 +35,7 @@ export function TestRunAgainButton({ clientId, scopes, redirectUri, variant, siz
3635
});
3736
return;
3837
}
39-
router.push(result.data.authorizationUrl);
38+
navigateTo(result.data.authorizationUrl);
4039
} catch (error) {
4140
const description = error instanceof Error ? error.message : "Unknown error";
4241
toast({ variant: "destructive", title: "Unable to restart test", description });

src/app/admin/clients/__tests__/test-run-again-button.test.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TestRunAgainButton } from "../[clientId]/test/test-run-again-button";
77

88
const mockPrepare = vi.hoisted(() => vi.fn().mockResolvedValue({ data: { authorizationUrl: "https://auth.example.test" } }));
99
const mockToast = vi.hoisted(() => vi.fn());
10-
const mockPush = vi.hoisted(() => vi.fn());
10+
const mockNavigate = vi.hoisted(() => vi.fn());
1111

1212
vi.mock("@/app/admin/actions", () => ({
1313
prepareClientOauthTestAction: mockPrepare,
@@ -17,15 +17,15 @@ vi.mock("@/components/ui/use-toast", () => ({
1717
useToast: () => ({ toast: mockToast }),
1818
}));
1919

20-
vi.mock("next/navigation", () => ({
21-
useRouter: () => ({ push: mockPush }),
20+
vi.mock("@/app/admin/clients/[clientId]/test/navigation", () => ({
21+
navigateTo: mockNavigate,
2222
}));
2323

2424
describe("TestRunAgainButton", () => {
2525
beforeEach(() => {
2626
mockPrepare.mockClear();
2727
mockToast.mockClear();
28-
mockPush.mockClear();
28+
mockNavigate.mockClear();
2929
});
3030

3131
it("restarts the OAuth test using prior settings", async () => {
@@ -41,7 +41,7 @@ describe("TestRunAgainButton", () => {
4141
redirectUri: "https://admin.example.test/callback",
4242
promptLogin: false,
4343
});
44-
expect(mockPush).toHaveBeenCalledWith("https://auth.example.test");
44+
expect(mockNavigate).toHaveBeenCalledWith("https://auth.example.test");
4545
});
4646
});
4747

@@ -58,7 +58,7 @@ describe("TestRunAgainButton", () => {
5858
title: "Unable to restart test",
5959
description: "Client missing",
6060
});
61-
expect(mockPush).not.toHaveBeenCalled();
61+
expect(mockNavigate).not.toHaveBeenCalled();
6262
});
6363
});
6464

@@ -75,7 +75,7 @@ describe("TestRunAgainButton", () => {
7575
title: "Unable to restart test",
7676
description: "Network down",
7777
});
78-
expect(mockPush).not.toHaveBeenCalled();
78+
expect(mockNavigate).not.toHaveBeenCalled();
7979
});
8080
});
8181

@@ -91,7 +91,7 @@ describe("TestRunAgainButton", () => {
9191

9292
await waitFor(() => {
9393
expect(mockPrepare).toHaveBeenCalled();
94-
expect(mockPush).toHaveBeenCalledWith("https://auth.example.test");
94+
expect(mockNavigate).toHaveBeenCalledWith("https://auth.example.test");
9595
});
9696
});
9797
});

src/app/r/[apiResourceId]/oidc/login/submit/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { z } from "zod";
33

4+
import { env } from "@/server/env";
45
import { toResponse } from "@/server/errors";
56
import { findOrCreateMockUser } from "@/server/services/mock-user-service";
67
import { createSession, MOCK_SESSION_COOKIE } from "@/server/services/mock-session-service";
@@ -125,7 +126,7 @@ export async function POST(request: NextRequest, context: ApiResourceRouteContex
125126
httpOnly: true,
126127
sameSite: "lax",
127128
secure: isSecure,
128-
maxAge: 60 * 60 * 12,
129+
maxAge: env.MOCKAUTH_SESSION_TTL_SECONDS,
129130
});
130131
const reauthCookieValue = createReauthCookieValue({
131132
tenantId: tenant.id,

src/server/auth/__tests__/options.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const baseEnv: AppEnv = {
1717
ENABLE_TEST_ROUTES: true,
1818
ALLOW_EMAIL_LINKING: false,
1919
MOCKAUTH_ALLOW_INSECURE_TEST_COOKIE: false,
20+
MOCKAUTH_SESSION_TTL_SECONDS: 2592000,
2021
AUDIT_LOG_RETENTION_DAYS: 90,
2122
};
2223

src/server/env.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ const envSchema = z.object({
3535
.optional()
3636
.default("false")
3737
.transform((value) => value === "true"),
38+
MOCKAUTH_SESSION_TTL_SECONDS: z.preprocess(
39+
(value) => (value === undefined ? undefined : Number(value)),
40+
z.number().int().min(1).default(2592000),
41+
),
3842
AUDIT_LOG_RETENTION_DAYS: z.preprocess(
3943
(value) => (value === undefined ? undefined : Number(value)),
4044
z.number().int().min(1).default(90),
@@ -56,6 +60,7 @@ const buildRawEnv = () => {
5660
ENABLE_TEST_ROUTES: process.env.ENABLE_TEST_ROUTES,
5761
ALLOW_EMAIL_LINKING: process.env.ALLOW_EMAIL_LINKING,
5862
MOCKAUTH_ALLOW_INSECURE_TEST_COOKIE: process.env.MOCKAUTH_ALLOW_INSECURE_TEST_COOKIE,
63+
MOCKAUTH_SESSION_TTL_SECONDS: process.env.MOCKAUTH_SESSION_TTL_SECONDS,
5964
AUDIT_LOG_RETENTION_DAYS: process.env.AUDIT_LOG_RETENTION_DAYS,
6065
CRON_SECRET: process.env.CRON_SECRET,
6166
} satisfies Record<string, string | undefined>;

src/server/oidc/__tests__/oidc-flow.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ describe("OIDC flow", () => {
271271
expect(violation?.details).toMatchObject({ reason: "pkce_mismatch" });
272272
});
273273

274-
it("requires an explicit login step before issuing codes", async () => {
274+
it("reuses the session when prompt is absent", async () => {
275275
const challenge = computeS256Challenge(codeVerifier);
276276
const returnTo = `https://mockauth.test/r/${apiResourceId}/oidc/authorize?client_id=${CLIENT_ID}`;
277277
const authorize = await handleAuthorize(
@@ -290,9 +290,9 @@ describe("OIDC flow", () => {
290290
returnTo,
291291
);
292292

293-
expect(authorize.type).toBe("login");
294-
expect(authorize.redirectTo).toContain("return_to=");
295-
expect(decodeURIComponent(authorize.redirectTo.split("return_to=")[1]!)).toBe(returnTo);
293+
expect(authorize.type).toBe("redirect");
294+
const redirectUrl = new URL(authorize.redirectTo);
295+
expect(redirectUrl.searchParams.get("code")).toBeTruthy();
296296
});
297297

298298
it("rejects invalid redirect_uri values", async () => {
@@ -571,7 +571,7 @@ describe("OIDC flow", () => {
571571
expect(authorize.type).toBe("login");
572572
});
573573

574-
it("requires login when the cookie is expired", async () => {
574+
it("reuses the session when the reauth cookie is expired", async () => {
575575
vi.useFakeTimers();
576576
try {
577577
vi.setSystemTime(new Date("2024-01-01T00:00:00.000Z"));
@@ -594,7 +594,9 @@ describe("OIDC flow", () => {
594594
`https://mockauth.test/r/${apiResourceId}/oidc/authorize?client_id=${CLIENT_ID}`,
595595
);
596596

597-
expect(authorize.type).toBe("login");
597+
expect(authorize.type).toBe("redirect");
598+
const redirectUrl = new URL(authorize.redirectTo);
599+
expect(redirectUrl.searchParams.get("code")).toBeTruthy();
598600
} finally {
599601
vi.useRealTimers();
600602
}

0 commit comments

Comments
 (0)