From 9ea2d3325d33e332c7333edc194425049afbffb8 Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Sun, 21 Jun 2026 00:12:45 +0200 Subject: [PATCH] feat(auth): enhance authentication flows with embedded support and UI improvements - Updated login and signup components to utilize AuthCard for better user experience during redirection. - Introduced AuthentikEmbedDialog for seamless integration of Authentik's identity portal within the application. - Enhanced password recovery and signup flows with dynamic theme handling and improved loading states. - Refactored existing components to streamline authentication processes and improve maintainability. --- app/api/auth/login/route.web.ts | 54 +++++- app/api/auth/session/route.web.ts | 5 +- .../sections/file-policies-section.tsx | 9 +- .../settings/sections/quotas-section.tsx | 6 +- components/agenda/agenda-sidebar.tsx | 7 +- components/auth/auth-flow-page.tsx | 147 ++++++++------- components/auth/auth-provider.tsx | 43 +++-- .../auth/forgot-password-page-content.tsx | 76 ++++++-- components/auth/login-page-content.tsx | 115 ++++++++---- components/auth/signup-page-content.tsx | 68 +++++-- components/compte/authentik-embed-dialog.tsx | 95 ++++++++++ components/compte/compte-authentik-panel.tsx | 45 ++--- lib/agenda/use-visible-agenda-calendars.ts | 7 +- lib/api/hooks/use-calendar-mutations.ts | 2 +- lib/api/hooks/use-calendar-queries.ts | 13 +- lib/auth/authentik-user-url.ts | 13 ++ lib/auth/clear-client-auth-state.ts | 21 +++ lib/auth/ensure-access-token.ts | 3 + lib/auth/flow-api.ts | 126 +++++++++++-- lib/auth/login-url.ts | 13 ++ lib/auth/oidc-config.ts | 21 ++- lib/auth/session.ts | 14 ++ lib/hooks/use-chrome-identity.ts | 2 + lib/stores/account-store.ts | 23 ++- lib/stores/debounced-json-storage.ts | 13 ++ next.config.mjs | 5 +- scripts/authentik-dom-dump-auth.mjs | 169 ++++++++++++++++++ scripts/authentik-dom-dump.mjs | 80 +++++++++ tsconfig.tsbuildinfo | 2 +- 29 files changed, 983 insertions(+), 214 deletions(-) create mode 100644 components/compte/authentik-embed-dialog.tsx create mode 100644 lib/auth/clear-client-auth-state.ts create mode 100644 scripts/authentik-dom-dump-auth.mjs create mode 100644 scripts/authentik-dom-dump.mjs diff --git a/app/api/auth/login/route.web.ts b/app/api/auth/login/route.web.ts index 6f49ee3..9e0b9c9 100644 --- a/app/api/auth/login/route.web.ts +++ b/app/api/auth/login/route.web.ts @@ -37,9 +37,19 @@ export async function GET(request: Request) { const requestUrl = new URL(request.url) const returnTo = requestUrl.searchParams.get("returnTo") ?? "/mail/inbox" const intent = requestUrl.searchParams.get("intent") + const bridge = requestUrl.searchParams.get("bridge") === "1" + // Embedded mode: the browser drives Authentik's flow executor (same origin) and authenticates + // the session in place, then navigates to the authorize URL. We just hand back the URL + the + // executor base, set the PKCE/state cookies, and never force a prompt. + const embedded = requestUrl.searchParams.get("embedded") === "1" + const promptParam = requestUrl.searchParams.get("prompt") const prompt = - requestUrl.searchParams.get("prompt") ?? - (intent === "add_account" ? "login select_account" : "select_account") + promptParam ?? + (bridge || embedded + ? null + : intent === "add_account" + ? "login select_account" + : "select_account") const jar = await cookies() const existingUser = platformUserFromToken( @@ -54,12 +64,16 @@ export async function GET(request: Request) { state, code_challenge: challenge, code_challenge_method: "S256", - prompt, }) + if (prompt) { + params.set("prompt", prompt) + } - const response = NextResponse.redirect( - `${cfg.authorizationEndpoint}?${params.toString()}` - ) + const authorizeUrl = `${cfg.authorizationEndpoint}?${params.toString()}` + + const response = embedded + ? NextResponse.json(buildEmbeddedContext(authorizeUrl)) + : NextResponse.redirect(authorizeUrl) const cookieOpts = oauthCookieOptions() response.cookies.set(PKCE_COOKIE, verifier, cookieOpts) response.cookies.set(STATE_COOKIE, state, cookieOpts) @@ -73,3 +87,31 @@ export async function GET(request: Request) { return response } + +/** + * Build the same-origin flow executor base + `next` query from a public authorize URL. + * The browser drives the flow at `${executorBase}/${slug}/?query=${flowQuery}`, then navigates + * to the returned `to` (the authorize URL) to obtain the code with its authenticated session. + */ +function buildEmbeddedContext(authorizeUrl: string): { + authorizeUrl: string + flowQuery: string + executorBase: string +} { + let executorBase = "/auth/api/v3/flows/executor" + let next = authorizeUrl + try { + const parsed = new URL(authorizeUrl) + const idx = parsed.pathname.indexOf("/application/") + const prefix = idx >= 0 ? parsed.pathname.slice(0, idx) : "/auth" + executorBase = `${prefix}/api/v3/flows/executor` + next = `${parsed.pathname}${parsed.search}` + } catch { + // keep defaults + } + return { + authorizeUrl, + flowQuery: `next=${encodeURIComponent(next)}`, + executorBase, + } +} diff --git a/app/api/auth/session/route.web.ts b/app/api/auth/session/route.web.ts index 9b9275f..cb23103 100644 --- a/app/api/auth/session/route.web.ts +++ b/app/api/auth/session/route.web.ts @@ -10,6 +10,7 @@ import { isAccessTokenValid, isIdTokenJwtValid, resolveBearerToken, + resolveSessionExpiresAt, } from "@/lib/auth/session" export async function GET() { @@ -23,7 +24,7 @@ export async function GET() { } if (isAccessTokenValid(accessToken, expiresAtRaw)) { - const expiresAt = Number(expiresAtRaw) + const expiresAt = resolveSessionExpiresAt(accessToken, expiresAtRaw) const user = platformUserFromToken(accessToken!) return NextResponse.json({ authenticated: true, @@ -47,7 +48,7 @@ export async function GET() { bearer = resolveBearerToken(tokens) } catch { if (accessToken && isIdTokenJwtValid(accessToken)) { - const expiresAt = Number(expiresAtRaw) || computeExpiresAt(3600) + const expiresAt = resolveSessionExpiresAt(accessToken, expiresAtRaw) const user = platformUserFromToken(accessToken) return NextResponse.json({ authenticated: true, diff --git a/components/admin/settings/sections/file-policies-section.tsx b/components/admin/settings/sections/file-policies-section.tsx index 69e6f89..ac8d863 100644 --- a/components/admin/settings/sections/file-policies-section.tsx +++ b/components/admin/settings/sections/file-policies-section.tsx @@ -61,10 +61,11 @@ export function FilePoliciesSection() { - + setFilePolicies({ max_upload_mib: Number(e.target.value) || 1 }) @@ -76,10 +77,11 @@ export function FilePoliciesSection() { - + setFilePolicies({ @@ -93,10 +95,11 @@ export function FilePoliciesSection() { - + setFilePolicies({ retention_trash_days: Number(e.target.value) || 1 }) diff --git a/components/admin/settings/sections/quotas-section.tsx b/components/admin/settings/sections/quotas-section.tsx index 12e1d8b..4e7c85d 100644 --- a/components/admin/settings/sections/quotas-section.tsx +++ b/components/admin/settings/sections/quotas-section.tsx @@ -16,6 +16,7 @@ import { InputGroupInput, InputGroupText, } from "@/components/ui/input-group" +import { cn } from "@/lib/utils" export function QuotasSection() { const storageQuotas = useOrgSettingsStore((s) => s.storageQuotas) @@ -60,7 +61,6 @@ export function QuotasSection() { min={50} max={100} fallback={90} - className="max-w-xs" value={storageQuotas.warn_threshold_pct} onChange={(v) => setStorageQuotas({ warn_threshold_pct: v })} /> @@ -95,7 +95,6 @@ export function QuotasSection() { min={50} max={100} fallback={80} - className="max-w-xs" value={usageQuotas.llm_cost_warn_threshold_pct} onChange={(v) => setUsageQuotas({ llm_cost_warn_threshold_pct: v })} /> @@ -179,13 +178,14 @@ function SettingsNumberField({ }) { return ( - + onChange(Number(e.target.value) || fallback)} /> diff --git a/components/agenda/agenda-sidebar.tsx b/components/agenda/agenda-sidebar.tsx index 1397a79..770725b 100644 --- a/components/agenda/agenda-sidebar.tsx +++ b/components/agenda/agenda-sidebar.tsx @@ -35,6 +35,7 @@ import { calendarColor } from "@/lib/agenda/agenda-events" import { useAgendaSettingsStore, useAgendaUIStore } from "@/lib/agenda/agenda-store" import type { AgendaCalendar } from "@/lib/agenda/agenda-types" import { useMergedAgendaCalendars } from "@/lib/agenda/use-visible-agenda-calendars" +import { usePersistHydrated } from "@/hooks/use-persist-hydrated" import { useIsMobile } from "@/hooks/use-mobile" import { cn } from "@/lib/utils" @@ -48,9 +49,11 @@ export function AgendaSidebar({ onCreateEvent: () => void }) { const isMobile = useIsMobile() + const settingsHydrated = usePersistHydrated(useAgendaSettingsStore) const sidebarCollapsed = useAgendaUIStore((s) => s.sidebarCollapsed) const setSidebarCollapsed = useAgendaUIStore((s) => s.setSidebarCollapsed) - const hiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds) + const storedHiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds) + const hiddenIds = settingsHydrated ? storedHiddenIds : [] const weekStart = useAgendaSettingsStore((s) => s.weekStart) const calendarViews = useAgendaSettingsStore((s) => s.calendarViews) const activeCalendarViewId = useAgendaSettingsStore((s) => s.activeCalendarViewId) @@ -144,7 +147,7 @@ export function AgendaSidebar({ - {calendarViews.length > 0 ? ( + {settingsHydrated && calendarViews.length > 0 ? ( <>