feat(web): support a generic OIDC login provider via env vars - #430
feat(web): support a generic OIDC login provider via env vars#430glifocat wants to merge 3 commits into
Conversation
Greptile SummaryThis PR adds a generic OIDC login provider (Keycloak, Authentik, Pocket ID, etc.) alongside the existing Google OAuth path, activated by three new env vars (
Confidence Score: 5/5Safe to merge — the OIDC path is additive, existing Google behavior is unchanged, and the previously-flagged JSON escaping issue in the entrypoint has been addressed. The change is purely additive: new env vars enable a new provider, all three detection points (nextauth config, runtime config, setup-error guard) are updated consistently, and the entrypoint correctly escapes free-form input before writing JSON. No existing auth paths are modified. No files require special attention.
|
| Filename | Overview |
|---|---|
| apps/web/src/lib/auth/nextauth-config.ts | Replaces the single-provider conditional with a dynamic providers array; adds a generic OIDC provider with pkce/state/nonce checks when the three OIDC env vars are all present. Google stays first for back-compat. |
| docker/entrypoint.sh | Adds authProviderId and authProviderName fields to the runtime-config.json write; OIDC_PROVIDER_NAME is now properly escaped for JSON via sed before interpolation, addressing the previously-flagged injection concern. |
| apps/web/src/lib/runtime-config.ts | Extends RuntimeConfig with authProviderId/authProviderName; both the Docker file-parse path and the local-dev env-var fallback populate these fields correctly, with Google taking priority when both providers are configured. |
| apps/web/src/proxy.ts | Updates the oauth-misconfigured check to accept either Google or OIDC as a valid provider; oidcConfigured is now properly boolean-cast with !!(). |
| apps/web/src/lib/auth/login-content.tsx | Renders a dynamic login button based on authProviderId/authProviderName from context; Google keeps its branded styling while any other OIDC provider gets a neutral button. Minor: className merging uses a raw ternary instead of cn(). |
| apps/web/src/lib/auth/auth-provider.tsx | Threads AuthProviderInfo through OAuthInner so nextAuthSignIn is called with the correct provider id; authProviderId/authProviderName are exposed on AuthContext for the login button. |
| apps/web/src/lib/auth/auth-mode.ts | Adds AuthProviderInfo interface and getAuthProvider() helper that reads authProviderId/authProviderName from runtime config. |
| apps/web/src/lib/env.ts | Adds OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET (defaulting to ""), and OIDC_PROVIDER_NAME (defaulting to "SSO") from process.env. |
| turbo.json | Declares the four new OIDC env vars so turbo/no-undeclared-env-vars stays clean. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User Browser
participant N as Next.js (proxy)
participant L as Login Page
participant A as NextAuth /api/auth
participant I as OIDC IdP
U->>N: GET /auth/login
N->>N: "getSetupError()<br/>(checks NEXTAUTH_SECRET + Google/OIDC)"
N-->>L: render (authProviderId, authProviderName via runtime-config)
L-->>U: "Continue with [providerName]" button
U->>L: click sign-in
L->>A: nextAuthSignIn("oidc")
A->>I: "Authorization Request<br/>(PKCE + state + nonce)"
I-->>U: IdP login UI
U->>I: authenticate
I->>A: "callback /api/auth/callback/oidc<br/>(code + state)"
A->>I: token exchange (PKCE verify)
I-->>A: id_token (nonce verified)
A-->>U: session cookie (JWT strategy)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as User Browser
participant N as Next.js (proxy)
participant L as Login Page
participant A as NextAuth /api/auth
participant I as OIDC IdP
U->>N: GET /auth/login
N->>N: "getSetupError()<br/>(checks NEXTAUTH_SECRET + Google/OIDC)"
N-->>L: render (authProviderId, authProviderName via runtime-config)
L-->>U: "Continue with [providerName]" button
U->>L: click sign-in
L->>A: nextAuthSignIn("oidc")
A->>I: "Authorization Request<br/>(PKCE + state + nonce)"
I-->>U: IdP login UI
U->>I: authenticate
I->>A: "callback /api/auth/callback/oidc<br/>(code + state)"
A->>I: token exchange (PKCE verify)
I-->>A: id_token (nonce verified)
A-->>U: session cookie (JWT strategy)
Reviews (2): Last reviewed commit: "fix: escape the OIDC provider label in r..." | Re-trigger Greptile
Adds OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, and OIDC_PROVIDER_NAME next to the existing Google provider. Google stays first when both are configured. The provider sets explicit pkce/state/nonce checks so spec-strict IdPs accept the flow, and the setup-error page copy now covers both provider options.
205e9b7 to
9a64c51
Compare
|
Ran this branch against Authentik on a live self-hosted instance behind TLS (merged onto current
{"authMode":"oauth","oauthConfigured":true,"authProviderId":"oidc","authProviderName":"Authentik"}Two notes from the integration.
|
I have read the CONTRIBUTING.md file.
YES
What kind of change does this PR introduce?
Feature. Discussed in #181.
What is the current behavior?
Self-hosted OAuth mode only works with Google (checked at v1.41.0,
fe0981f0). The provider is hardcoded in three places: the NextAuth configonly builds a
Googleprovider(
apps/web/src/lib/auth/nextauth-config.ts), the sign-in call isnextAuthSignIn("google")(apps/web/src/lib/auth/auth-provider.tsx), andthe startup check treats "no
GOOGLE_CLIENT_ID" as misconfigured(
apps/web/src/proxy.ts). So a self-hoster with Keycloak, Authentik, PocketID, or Dex cannot use their own identity provider, and fully offline
deployments cannot use multi-user mode at all (#181).
What is the new behavior?
Four new env vars configure a generic OIDC provider next to Google:
OIDC_ISSUER,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET: standard OIDCdiscovery. The provider is active when all three are set.
OIDC_PROVIDER_NAME: login button label, defaults to "SSO".Behavior:
existing deployments see no difference.
runtime-config mechanism (entrypoint-written
runtime-config.jsoninDocker, env fallback in local dev). The login button label follows
OIDC_PROVIDER_NAME; Google keeps its branded button.oauth-misconfiguredsetup error now fires only whenNEXTAUTH_SECRETis set and neither Google nor OIDC is configured.<APP_URL>/api/auth/callback/oidc.checks: ["pkce", "state", "nonce"]explicitly. Withoutit, Auth.js 5.0.0-beta.30 builds the authorize URL with only
code_challenge, and spec-strict IdPs reject the callback withinvalid_state(I hit this with Pocket ID before adding it). Lenient IdPsaccept either; strict ones need this.
/setup-errorpage copy now describes both provider options insteadof only the Google vars, matching the updated check.
.env.exampleand the README, and declared inturbo.jsonsoturbo/no-undeclared-env-varsstays clean.One known limitation, kept deliberately to keep the diff small: the login
page still renders a single button. When both Google and OIDC are configured
the button starts the Google flow; the OIDC flow is still reachable at
/api/auth/signin/oidc. If you would rather have one button per configuredprovider I am happy to extend the login page in this PR or a follow-up.
How I verified it
pnpm check-types(apps/web) passes on the branch.eslint --max-warnings 0passes on all changed files.pnpm dev, OIDC-onlyconfig, no Google vars set): the login page renders "Continue with Pocket
ID", the full flow completes (authorize, passkey login, callback, session),
sign-out and re-login work, and the session sync upserts the user with the
OIDC
subasexternal_auth_idand the IdP email (checked in Postgres).With
NEXTAUTH_SECRETset and no provider configured, the setup-errorpage shows as expected.
http://localhost; https issuers are the normal case) and theboth-providers-configured combination beyond unit-level reasoning.
Additional context
behavior, and the Cognito path does not go through this code.
NEXTAUTH_URLset to the external origin.The Docker image already ships
AUTH_TRUST_HOST=true, so no code changewas needed for proxy support;
.env.examplenotes it.docker/entrypoint.shinterpolatesOIDC_PROVIDER_NAMEintoruntime-config.jsonthe same way the existing fields are written, so alabel containing a double quote would break the JSON. That matches the
current pattern for the other fields; I can add escaping if you want it.
Assisted by Claude; reviewed and verified by a human before submission.