Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/admin-ui/src/pages/Branding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function buildPreviewUrl(
};
const o = toB64Url(JSON.stringify(options));
const u = encodeURIComponent(issuer);
return `/preview?options=${o}&u=${u}`;
return `/preview?options=${o}&u=${u}&da_preview=1`;
}

function _inferType(s: AdminSetting): "string" | "number" | "boolean" | "object" {
Expand Down
32 changes: 28 additions & 4 deletions packages/admin-ui/src/pages/ClientEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export default function ClientEdit({ mode = "edit" }: ClientEditProps) {
allowedJweAlgs: "",
allowedJweEncs: "",
idTokenLifetimeSeconds: "",
accessTokenLifetimeSeconds: "",
refreshTokenLifetimeSeconds: "",
});

Expand Down Expand Up @@ -194,6 +195,9 @@ export default function ClientEdit({ mode = "edit" }: ClientEditProps) {
allowedJweAlgs: joinList(c.allowedJweAlgs),
allowedJweEncs: joinList(c.allowedJweEncs),
idTokenLifetimeSeconds: c.idTokenLifetimeSeconds ? String(c.idTokenLifetimeSeconds) : "",
accessTokenLifetimeSeconds: c.accessTokenLifetimeSeconds
? String(c.accessTokenLifetimeSeconds)
: "",
refreshTokenLifetimeSeconds: c.refreshTokenLifetimeSeconds
? String(c.refreshTokenLifetimeSeconds)
: "",
Expand Down Expand Up @@ -251,6 +255,9 @@ export default function ClientEdit({ mode = "edit" }: ClientEditProps) {
idTokenLifetimeSeconds: form.idTokenLifetimeSeconds
? Number(form.idTokenLifetimeSeconds)
: undefined,
accessTokenLifetimeSeconds: form.accessTokenLifetimeSeconds
? Number(form.accessTokenLifetimeSeconds)
: undefined,
refreshTokenLifetimeSeconds: form.refreshTokenLifetimeSeconds
? Number(form.refreshTokenLifetimeSeconds)
: undefined,
Expand Down Expand Up @@ -834,17 +841,15 @@ export default function ClientEdit({ mode = "edit" }: ClientEditProps) {
<Card className={styles.tabCard}>
<CardHeader>
<CardTitle>Token Policy</CardTitle>
<CardDescription>
Optional token lifetime overrides for this client.
</CardDescription>
<CardDescription>Token lifetime configuration for this client.</CardDescription>
</CardHeader>
<CardContent>
<FormGrid columns={1}>
<FormField
label={
<FieldLabel
title="ID Token TTL (seconds)"
tooltip="Overrides global ID token lifetime for this client. Leave empty to use system defaults."
tooltip="ID token lifetime for this client."
/>
}
>
Expand All @@ -859,6 +864,25 @@ export default function ClientEdit({ mode = "edit" }: ClientEditProps) {
<FieldHint>Typical values: 300 to 3600 seconds.</FieldHint>
</FormField>

<FormField
label={
<FieldLabel
title="Access Token TTL (seconds)"
tooltip="Access token lifetime for this client."
/>
}
>
<Input
type="number"
min={60}
value={form.accessTokenLifetimeSeconds}
onChange={(e) =>
setForm((f) => ({ ...f, accessTokenLifetimeSeconds: e.target.value }))
}
/>
<FieldHint>Typical values: 300 to 3600 seconds.</FieldHint>
</FormField>

<FormField
label={
<FieldLabel
Expand Down
35 changes: 2 additions & 33 deletions packages/admin-ui/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,6 @@ export interface AdminOpaqueLoginFinishResponse {
};
}

export interface AdminOpaqueRegisterStartRequest {
email: string;
request: string; // base64url encoded
}

export interface AdminOpaqueRegisterStartResponse {
message: string; // base64url encoded
serverPublicKey: string; // base64url encoded
}

export interface AdminOpaqueRegisterFinishRequest {
email: string;
name: string;
role: "read" | "write";
record: string; // base64url encoded
}

export interface AdminSessionResponse {
authenticated: boolean;
adminId?: string;
Expand Down Expand Up @@ -220,6 +203,7 @@ export interface Client {
updatedAt: string;
clientSecret?: string; // Only returned when creating/updating
idTokenLifetimeSeconds?: number;
accessTokenLifetimeSeconds?: number;
refreshTokenLifetimeSeconds?: number;
}

Expand Down Expand Up @@ -252,6 +236,7 @@ export interface CreateClientRequest {
scopes?: Array<string | ClientScope>;
allowedZkOrigins?: string[];
idTokenLifetimeSeconds?: number;
accessTokenLifetimeSeconds?: number;
refreshTokenLifetimeSeconds?: number;
}

Expand Down Expand Up @@ -474,22 +459,6 @@ class AdminApiService {
});
}

async adminOpaqueRegisterStart(
request: AdminOpaqueRegisterStartRequest
): Promise<AdminOpaqueRegisterStartResponse> {
return this.request("/opaque/register/start", {
method: "POST",
body: JSON.stringify(request),
});
}

async adminOpaqueRegisterFinish(request: AdminOpaqueRegisterFinishRequest): Promise<void> {
return this.request("/opaque/register/finish", {
method: "POST",
body: JSON.stringify(request),
});
}

// Session Management
async getAdminSession(): Promise<AdminSessionResponse> {
return this.request("/session");
Expand Down
1 change: 1 addition & 0 deletions packages/api/drizzle/0014_client_access_token_ttl.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "clients" ADD COLUMN "access_token_lifetime_seconds" integer;
7 changes: 7 additions & 0 deletions packages/api/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"when": 1772269200000,
"tag": "0013_client_dashboard_icons",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1772323200000,
"tag": "0014_client_access_token_ttl",
"breakpoints": true
}
]
}
6 changes: 6 additions & 0 deletions packages/api/src/context/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ensureDefaultOrganizationAndSchema } from "../models/install.ts";
import { ensureKekService } from "../services/kek.ts";
import { createOpaqueService } from "../services/opaque.ts";
import { cleanupExpiredSessions } from "../services/sessions.ts";
import { pruneDeprecatedSettings } from "../services/settings.ts";
import type { Config, Context, Database } from "../types.ts";

async function waitForPostgres(pool: Pool, attempts = 20, delayMs = 500) {
Expand Down Expand Up @@ -109,6 +110,11 @@ export async function createContext(config: Config): Promise<Context> {
} catch (err) {
logger.warn({ err }, "ensureDefaultOrganizationAndSchema failed");
}
try {
await pruneDeprecatedSettings(context);
} catch (err) {
logger.warn({ err }, "pruneDeprecatedSettings failed");
}
}

if (!config.inInstallMode) {
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/controllers/admin/clientCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const CreateClientSchema = z.object({
]),
allowedZkOrigins: z.array(z.string()).optional().default([]),
idTokenLifetimeSeconds: z.number().int().positive().optional(),
accessTokenLifetimeSeconds: z.number().int().positive().optional(),
refreshTokenLifetimeSeconds: z.number().int().positive().optional(),
});

Expand All @@ -70,6 +71,7 @@ export const ClientResponseSchema = z.object({
scopes: z.array(ScopeSchema),
allowedZkOrigins: z.array(z.string()),
idTokenLifetimeSeconds: z.number().int().positive().nullable(),
accessTokenLifetimeSeconds: z.number().int().positive().nullable(),
refreshTokenLifetimeSeconds: z.number().int().positive().nullable(),
createdAt: z.date().or(z.string()),
updatedAt: z.date().or(z.string()),
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/controllers/admin/clientUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async function updateClientHandler(
scopes: z.array(z.union([z.string().min(1), ScopeSchema])).optional(),
allowedZkOrigins: z.array(z.string()).optional(),
idTokenLifetimeSeconds: z.number().int().positive().nullable().optional(),
accessTokenLifetimeSeconds: z.number().int().positive().nullable().optional(),
refreshTokenLifetimeSeconds: z.number().int().positive().nullable().optional(),
});
const parsedUpdates = Req.parse(parsed as unknown);
Expand Down Expand Up @@ -142,6 +143,7 @@ const Req = z.object({
scopes: z.array(z.union([z.string().min(1), ScopeSchema])).optional(),
allowedZkOrigins: z.array(z.string()).optional(),
idTokenLifetimeSeconds: z.number().int().positive().nullable().optional(),
accessTokenLifetimeSeconds: z.number().int().positive().nullable().optional(),
refreshTokenLifetimeSeconds: z.number().int().positive().nullable().optional(),
});

Expand Down
1 change: 1 addition & 0 deletions packages/api/src/controllers/admin/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const ClientResponseSchema = z.object({
scopes: z.array(ScopeSchema),
allowedZkOrigins: z.array(z.string()),
idTokenLifetimeSeconds: z.number().int().positive().nullable(),
accessTokenLifetimeSeconds: z.number().int().positive().nullable(),
refreshTokenLifetimeSeconds: z.number().int().positive().nullable(),
createdAt: z.date().or(z.string()),
updatedAt: z.date().or(z.string()),
Expand Down
8 changes: 7 additions & 1 deletion packages/api/src/controllers/admin/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import { z } from "zod/v4";
import { genericErrors } from "../../http/openapi-helpers.ts";

import { clearSessionCookies, deleteSession, getSessionId } from "../../services/sessions.ts";
import {
clearRefreshTokenCookie,
clearSessionCookies,
deleteSession,
getSessionId,
} from "../../services/sessions.ts";
import type { Context, ControllerSchema } from "../../types.ts";
import { withAudit } from "../../utils/auditWrapper.ts";
import { sendJson } from "../../utils/http.ts";
Expand All @@ -19,6 +24,7 @@ async function postAdminLogoutHandler(
await deleteSession(context, sessionId);
}
clearSessionCookies(response, true);
clearRefreshTokenCookie(response, true);

sendJson(response, 200, {
success: true,
Expand Down
68 changes: 0 additions & 68 deletions packages/api/src/controllers/admin/refreshToken.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/api/src/controllers/admin/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { genericErrors } from "../../http/openapi-helpers.ts";
import { listSettings } from "../../models/settings.ts";
import { ensureBrandingDefaults } from "../../services/branding.ts";
import { requireSession } from "../../services/sessions.ts";
import { pruneDeprecatedSettings } from "../../services/settings.ts";
import type { Context, ControllerSchema } from "../../types.ts";
import { sendJson } from "../../utils/http.ts";

Expand All @@ -23,6 +24,7 @@ export async function getSettings(

// Get all settings
await ensureBrandingDefaults(context);
await pruneDeprecatedSettings(context);
const settingsData = await listSettings(context);

const flattened = settingsData;
Expand Down
4 changes: 0 additions & 4 deletions packages/api/src/controllers/admin/settingsUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ interface SettingsUpdateRequest {
const ALLOWED_SETTINGS = [
"rate_limits",
"security",
"code",
"pkce",
"id_token",
"access_token",
"zk_delivery",
"opaque",
"security_headers",
Expand Down
22 changes: 8 additions & 14 deletions packages/api/src/controllers/user/opaqueLoginFinish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,20 +202,14 @@ export const postOpaqueLoginFinish = withRateLimit("opaque", (body) => {
const refreshTtlSeconds = await getRefreshTokenTtlSeconds(context, "user");
issueSessionCookies(response, createdSessionId, ttlSeconds, false);
issueRefreshTokenCookie(response, refreshToken, refreshTtlSeconds, false);
let accessTokenTtl = 600;
const accessTokenSettings = (await getSetting(context, "access_token")) as
| { lifetime_seconds?: number }
| undefined
| null;
if (accessTokenSettings?.lifetime_seconds && accessTokenSettings.lifetime_seconds > 0) {
accessTokenTtl = accessTokenSettings.lifetime_seconds;
} else {
const flat = (await getSetting(context, "access_token.lifetime_seconds")) as
| number
| undefined
| null;
if (typeof flat === "number" && flat > 0) accessTokenTtl = flat;
}
const client = await (await import("../../models/clients.ts")).getClient(
context,
userClientId
);
const accessTokenTtl =
client?.accessTokenLifetimeSeconds && client.accessTokenLifetimeSeconds > 0
? client.accessTokenLifetimeSeconds
: 600;
const now = Math.floor(Date.now() / 1000);
const accessToken = await signJWT(
context,
Expand Down
1 change: 0 additions & 1 deletion packages/api/src/controllers/user/opaqueLoginStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export const postOpaqueLoginStart = withRateLimit("opaque", (body) =>
// Convert response to base64url for JSON transmission
const responseData = {
message: toBase64Url(Buffer.from(loginResponse.message)),
sub: "00000000-0000-0000-0000-000000000000",
sessionId: loginResponse.sessionId,
};

Expand Down
Loading
Loading