feat(auth): add Authentik OIDC provider + CAP_DISABLE_EMAIL_AUTH flag#1869
feat(auth): add Authentik OIDC provider + CAP_DISABLE_EMAIL_AUTH flag#1869stig-codes wants to merge 7 commits into
Conversation
NextAuth ships next-auth/providers/authentik but Cap does not currently wire it up, so Authentik users have no clean way to sign in to a self- hosted Cap. The provider is gated on AUTHENTIK_ISSUER being set, so Cap Cloud builds (and any deployment that has not configured Authentik) remain unchanged. Closes CapSoftware#914 (CAP-453)
Adds an authentikAuthAvailable boolean to publicEnv (gated on AUTHENTIK_ISSUER) and renders a Login/Sign-up with Authentik button alongside the existing Google and WorkOS options in apps/web/app/(org)/login/form.tsx, apps/web/app/(org)/signup/form.tsx, and apps/web/app/s/[videoId]/_components/ AuthOverlay.tsx. Mirrors the existing pattern; deployments without Authentik configured see no UI change. Refs CapSoftware#914 (CAP-453)
Adds an opt-in CAP_DISABLE_EMAIL_AUTH flag (boolean, default false) that hides the email input + Login/Sign up with email button on /login, /signup, and the share-page sign-in modal, plus the "Sign up here" link on /login. The EmailProvider stays wired server-side so the magic-link flow remains available as break-glass via direct API call. Useful for OIDC-only deployments. Refs CapSoftware#914 (CAP-453)
The OR separator between email and OAuth buttons is meaningless when the email block above it is hidden. Wrap the divider in the same !publicEnv.disableEmailAuth gate so an OIDC-only deployment renders a clean Authentik-only login form. Refs CapSoftware#914 (CAP-453)
…DISABLE_EMAIL_AUTH Two cleanups for OIDC-only deployments: 1. The "By typing your email and clicking continue..." Terms-of-Service line on /login and /signup is orphan copy when the email block is hidden. Wrap it in the same !publicEnv.disableEmailAuth gate. 2. With email auth disabled, OAuth users sign up via first sign-in (NextAuth auto-provisions through the database adapter), so /signup has no separate flow. Add a server-side redirect /signup -> /login when CAP_DISABLE_EMAIL_AUTH=true. Refs CapSoftware#914 (CAP-453)
| ...(serverEnv().AUTHENTIK_ISSUER | ||
| ? [ | ||
| AuthentikProvider({ | ||
| clientId: serverEnv().AUTHENTIK_CLIENT_ID as string, | ||
| clientSecret: serverEnv().AUTHENTIK_CLIENT_SECRET as string, | ||
| issuer: serverEnv().AUTHENTIK_ISSUER as string, | ||
| }), | ||
| ] | ||
| : []), |
There was a problem hiding this comment.
Missing companion-var validation for Authentik credentials
AUTHENTIK_ISSUER gates provider registration, but AUTHENTIK_CLIENT_ID and AUTHENTIK_CLIENT_SECRET are each independently optional in the schema. If a deployer sets only AUTHENTIK_ISSUER (or sets the issuer but misspells either credential key), serverEnv().AUTHENTIK_CLIENT_ID evaluates to undefined, the as string cast silences TypeScript, and AuthentikProvider is registered with empty credentials. The OIDC flow will fail at token exchange with a cryptic IdP error rather than a clear startup-time misconfiguration message.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/database/auth/auth-options.ts
Line: 73-81
Comment:
**Missing companion-var validation for Authentik credentials**
`AUTHENTIK_ISSUER` gates provider registration, but `AUTHENTIK_CLIENT_ID` and `AUTHENTIK_CLIENT_SECRET` are each independently optional in the schema. If a deployer sets only `AUTHENTIK_ISSUER` (or sets the issuer but misspells either credential key), `serverEnv().AUTHENTIK_CLIENT_ID` evaluates to `undefined`, the `as string` cast silences TypeScript, and `AuthentikProvider` is registered with empty credentials. The OIDC flow will fail at token exchange with a cryptic IdP error rather than a clear startup-time misconfiguration message.
How can I resolve this? If you propose a fix, please make it concise.| variant="gray" | ||
| type="button" | ||
| className="flex gap-2 justify-center items-center my-1 w-full text-sm" | ||
| onClick={() => signIn("authentik")} | ||
| disabled={loading} | ||
| > | ||
| <FontAwesomeIcon className="size-4" icon={faRightToBracket} /> | ||
| Login with Authentik |
There was a problem hiding this comment.
Authentik sign-in in
AuthOverlay drops the video callbackUrl
The Google handler in the same component sets callbackUrl: \${window.location.origin}/s/${videoId}`so the user is returned to the share page after OAuth. The Authentik button callssignIn("authentik")with no options, so NextAuth will redirect to its default post-sign-in URL (typically/dashboard`) instead of back to the video. A user clicking "Login with Authentik" on a share page will lose their place and the comment dialog will close.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/s/[videoId]/_components/AuthOverlay.tsx
Line: 247-254
Comment:
**Authentik sign-in in `AuthOverlay` drops the video `callbackUrl`**
The Google handler in the same component sets `callbackUrl: \`${window.location.origin}/s/${videoId}\`` so the user is returned to the share page after OAuth. The Authentik button calls `signIn("authentik")` with no options, so NextAuth will redirect to its default post-sign-in URL (typically `/dashboard`) instead of back to the video. A user clicking "Login with Authentik" on a share page will lose their place and the comment dialog will close.
How can I resolve this? If you propose a fix, please make it concise.| {publicEnv.authentikAuthAvailable && ( | ||
| <Button | ||
| variant="gray" | ||
| type="button" | ||
| className="flex gap-2 justify-center items-center my-1 w-full text-sm" | ||
| onClick={() => signIn("authentik")} | ||
| disabled={loading} | ||
| > | ||
| <FontAwesomeIcon className="size-4" icon={faRightToBracket} /> | ||
| Login with Authentik | ||
| </Button> | ||
| )} |
There was a problem hiding this comment.
Authentik sign-in missing analytics tracking
Every other auth method in this file calls trackEvent("auth_started", ...) before signIn. The Authentik button skips this, creating a gap in auth-surface analytics. It also doesn't set loading or redirect: false, making it inconsistent with the Google handler's pattern.
| {publicEnv.authentikAuthAvailable && ( | |
| <Button | |
| variant="gray" | |
| type="button" | |
| className="flex gap-2 justify-center items-center my-1 w-full text-sm" | |
| onClick={() => signIn("authentik")} | |
| disabled={loading} | |
| > | |
| <FontAwesomeIcon className="size-4" icon={faRightToBracket} /> | |
| Login with Authentik | |
| </Button> | |
| )} | |
| {publicEnv.authentikAuthAvailable && ( | |
| <Button | |
| variant="gray" | |
| type="button" | |
| className="flex gap-2 justify-center items-center my-1 w-full text-sm" | |
| onClick={() => { | |
| trackEvent("auth_started", { | |
| method: "authentik", | |
| is_signup: false, | |
| auth_surface: "share_overlay", | |
| video_id: videoId, | |
| }); | |
| setLoading(true); | |
| signIn("authentik", { | |
| callbackUrl: `${window.location.origin}/s/${videoId}`, | |
| }); | |
| }} | |
| disabled={loading} | |
| > | |
| <FontAwesomeIcon className="size-4" icon={faRightToBracket} /> | |
| Login with Authentik | |
| </Button> | |
| )} |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/s/[videoId]/_components/AuthOverlay.tsx
Line: 245-256
Comment:
**Authentik sign-in missing analytics tracking**
Every other auth method in this file calls `trackEvent("auth_started", ...)` before `signIn`. The Authentik button skips this, creating a gap in auth-surface analytics. It also doesn't set `loading` or `redirect: false`, making it inconsistent with the Google handler's pattern.
```suggestion
{publicEnv.authentikAuthAvailable && (
<Button
variant="gray"
type="button"
className="flex gap-2 justify-center items-center my-1 w-full text-sm"
onClick={() => {
trackEvent("auth_started", {
method: "authentik",
is_signup: false,
auth_surface: "share_overlay",
video_id: videoId,
});
setLoading(true);
signIn("authentik", {
callbackUrl: `${window.location.origin}/s/${videoId}`,
});
}}
disabled={loading}
>
<FontAwesomeIcon className="size-4" icon={faRightToBracket} />
Login with Authentik
</Button>
)}
```
How can I resolve this? If you propose a fix, please make it concise.…s set Address greptile-apps[bot] P1 review on PR CapSoftware#1869: previously the `AUTHENTIK_ISSUER` env gated provider registration, but `AUTHENTIK_CLIENT_ID` and `AUTHENTIK_CLIENT_SECRET` were each independently optional. A deployer who set the issuer but mistyped a companion key would silently register `AuthentikProvider` with empty credentials and only fail at the IdP token-exchange step with a cryptic error. Now all three vars are read once, and we throw a clear startup error if `AUTHENTIK_ISSUER` is set without the companions. Drops the unsafe `as string` casts that had been masking the type mismatch. Refs CapSoftware#1869
…thOverlay Address greptile-apps[bot] P1+P2 review on PR CapSoftware#1869: the share-page `AuthOverlay` Authentik button called `signIn("authentik")` with no options, so a user clicking it from a video share page would land on `/dashboard` instead of returning to the video. It also skipped the `trackEvent("auth_started", ...)` analytics call and the `setLoading` state setup that the Google handler in the same component performs. Replaces the inline onClick with a `handleAuthentikSignIn` handler that mirrors `handleGoogleSignIn`: tracks the auth_started event with `auth_surface: "share_overlay"` and `video_id`, sets loading, and passes `callbackUrl: \`${window.location.origin}/s/${videoId}\`` so the user returns to the share page after sign-in. Refs CapSoftware#1869
|
Thanks for the careful review! All three findings addressed in the two new commits on top:
Verified end-to-end against an Authentik instance:
|
Summary
Adds a NextAuth
authentikprovider to self-hosted Cap so OIDC-only deployments can sign in via Authentik (or any OIDC IdP that respects standard discovery), plus an optionalCAP_DISABLE_EMAIL_AUTHflag for OIDC-only deployments that want to hide the magic-link UI entirely.Closes #914 (CAP-453).
The patch is purely additive — every change is gated on a new env var, so deployments that don't set them see no behavior change. Cap Cloud and existing self-hosted users are unaffected.
What this changes
Authentik OIDC provider (
AUTHENTIK_ISSUER/_CLIENT_ID/_CLIENT_SECRET)packages/database/auth/auth-options.ts— spreads inAuthentikProviderfromnext-auth/providers/authentikwhenAUTHENTIK_ISSUERis set.packages/env/server.ts— adds three optional env keys.apps/web/utils/public-env.tsx+app/layout.tsx— surfacesauthentikAuthAvailableto client components.apps/web/app/(org)/login/form.tsx,app/(org)/signup/form.tsx,app/s/[videoId]/_components/AuthOverlay.tsx— render a "Login/Sign up with Authentik" button alongside the existing Google/WorkOS options, using the samepublicEnv.<provider>AuthAvailablepattern.CAP_DISABLE_EMAIL_AUTHflag (defaultfalse)For OIDC-only deployments. When
true:/signup→/login— OAuth users sign up via first sign-in via the database adapter.EmailProviderstays wired server-side as a break-glass option for direct API access; this is a UI-only toggle.Notes
authentik, but works against any OIDC-compliant IdP (Keycloak, Zitadel, Pocket ID, Authelia, etc.) by settingAUTHENTIK_ISSUERto that IdP's OIDC discovery URL.next-auth/providers/authentikdocumentation says the issuer should be the application slug URL without a trailing slash, so set e.g.AUTHENTIK_ISSUER=https://auth.example.com/application/o/cap(no trailing slash). The provider appends/.well-known/openid-configurationitself./api/auth/callback/authentik— matches NextAuth's convention; that's the URL to register in your Authentik provider config.Commits
5 focused commits:
publicEnv.authentikAuthAvailable.Test plan
pnpm i --frozen-lockfilesucceeds.pnpm run build:websucceeds (built locally ascap-web:lynton-authentik, 620 MB).AUTHENTIK_ISSUERset,/api/auth/providersreturns the existing{google, workos, email}set unchanged. Login UI renders as before.CAP_DISABLE_EMAIL_AUTH, the email block, OR divider, ToS line, and signup link all render unchanged.AUTHENTIK_ISSUERset:/api/auth/providersincludesauthentik;/loginshows "Login with Authentik"./api/auth/signin/authentikreturns a properly signed authorize URL with PKCEcode_challenge_method=S256, scopeopenid email profile, and the registered redirect_uri./dashboardreachable.CAP_DISABLE_EMAIL_AUTH=true:/signup307-redirects to/login;/loginshows only the Authentik button (no email block, no OR, no ToS prose, no signup link).cap-weblogs; no new TypeScript errors.Compatibility
next-auth/providers/authentikalready shipped withnext-auth@4.24.5.Greptile Summary
This PR adds an Authentik OIDC provider to self-hosted Cap deployments and a
CAP_DISABLE_EMAIL_AUTHflag that hides magic-link UI for OIDC-only setups. The changes are purely additive and env-gated, leaving existing deployments unaffected.packages/database/auth/auth-options.ts/packages/env/server.ts: RegistersAuthentikProviderwhenAUTHENTIK_ISSUERis set; adds three optional Authentik env vars and the booleanCAP_DISABLE_EMAIL_AUTHflag.login/form.tsx,signup/form.tsx,signup/page.tsx: Renders the Authentik button with analytics andcallbackUrlsupport; hides email inputs, OR divider, ToS prose, and signup link behinddisableEmailAuth; server-redirects/signup→/loginwhen the flag is on.AuthOverlay.tsx: Adds the Authentik button to the share-page comment overlay, but is missing acallbackUrlback to the video and omits analytics tracking — unlike the Google handler in the same file.Confidence Score: 3/5
The login and signup forms are well-implemented, but two issues in AuthOverlay.tsx and a misconfiguration risk in auth-options.ts need fixing before merge.
The AuthOverlay Authentik button drops the video callbackUrl, so users authenticating from a share page are redirected away from the video instead of back to it. Separately, AuthentikProvider is constructed using AUTHENTIK_CLIENT_ID/AUTHENTIK_CLIENT_SECRET cast to string with no validation that they are actually set — a partial configuration silently produces a broken provider whose failures only surface at token-exchange time.
apps/web/app/s/[videoId]/_components/AuthOverlay.tsx and packages/database/auth/auth-options.ts need attention before this is ready to merge.
Important Files Changed
Comments Outside Diff (1)
apps/web/app/s/[videoId]/_components/AuthOverlay.tsx, line 94-113 (link)The login and signup forms wrap their email-specific ToS paragraph in a
publicEnv.disableEmailAuthcheck. The matching paragraph inAuthOverlay(rendered outsideStepOne, always visible on step 1) has no such gate — so "By entering your email…" stays visible when email auth is disabled, even though the email input is hidden and only OAuth buttons are shown.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(auth): hide email-specific ToS line..." | Re-trigger Greptile