Summary
Adds SSO login via any OIDC-compliant identity provider using better-auth's genericOAuth plugin. One integration that works with Okta, Auth0, Keycloak, OneLogin, and any provider that exposes a standard OIDC discovery document.
Configuration
Set the following environment variables to enable:
OIDC_DISCOVERY_URL (required) — provider's .well-known/openid-configuration URL
OIDC_CLIENT_ID (required)
OIDC_CLIENT_SECRET (required)
OIDC_PROVIDER_ID — defaults to oidc, used in callback URL and button logic
OIDC_PROVIDER_NAME — defaults to SSO, shown on the login button
OIDC_SCOPES — defaults to openid,profile,email
OIDC_AUTH_DOMAINS — optional comma-separated email domain allowlist
OIDC_PKCE — defaults to true
When unset, the SSO button is hidden.
Redirect URI to register in your IdP: https://<your-nao-host>/api/auth/oauth2/callback/{OIDC_PROVIDER_ID}
What's included
- Backend:
genericOAuth plugin registration, env schema (8 vars), domain allowlist enforcement in databaseHooks.user.create.before, OIDC users included in isSocial check for auto-provisioning, tRPC authConfig.oidc.getConfig endpoint
- Frontend:
genericOAuthClient plugin, dynamic handleOidcSignIn via signIn.oauth2, conditional "Continue with {providerName}" button with LockKeyholeIcon
- Docs: setup guide at
apps/backend/docs/auth-oidc.md with walkthroughs for Okta, Auth0, Keycloak, and OneLogin
- Tests: 15 Vitest unit tests — 10 for domain allowlist edge cases, 5 for tRPC endpoint
Test plan
Summary
Adds SSO login via any OIDC-compliant identity provider using better-auth's
genericOAuthplugin. One integration that works with Okta, Auth0, Keycloak, OneLogin, and any provider that exposes a standard OIDC discovery document.Configuration
Set the following environment variables to enable:
OIDC_DISCOVERY_URL(required) — provider's.well-known/openid-configurationURLOIDC_CLIENT_ID(required)OIDC_CLIENT_SECRET(required)OIDC_PROVIDER_ID— defaults tooidc, used in callback URL and button logicOIDC_PROVIDER_NAME— defaults toSSO, shown on the login buttonOIDC_SCOPES— defaults toopenid,profile,emailOIDC_AUTH_DOMAINS— optional comma-separated email domain allowlistOIDC_PKCE— defaults totrueWhen unset, the SSO button is hidden.
Redirect URI to register in your IdP:
https://<your-nao-host>/api/auth/oauth2/callback/{OIDC_PROVIDER_ID}What's included
genericOAuthplugin registration, env schema (8 vars), domain allowlist enforcement indatabaseHooks.user.create.before, OIDC users included inisSocialcheck for auto-provisioning, tRPCauthConfig.oidc.getConfigendpointgenericOAuthClientplugin, dynamichandleOidcSignInviasignIn.oauth2, conditional "Continue with {providerName}" button withLockKeyholeIconapps/backend/docs/auth-oidc.mdwith walkthroughs for Okta, Auth0, Keycloak, and OneLoginTest plan
npm run lintpasses (no new errors — pre-existing drizzle-orm type issues unrelated)npm testpasses (15/15 new tests pass)/