Skip to content

feat: multi-provider OAuth support (Google, Microsoft, Auth0, Keycloak)#353

Closed
karpikpl wants to merge 15 commits intomainfrom
copilot/fix-244-v2
Closed

feat: multi-provider OAuth support (Google, Microsoft, Auth0, Keycloak)#353
karpikpl wants to merge 15 commits intomainfrom
copilot/fix-244-v2

Conversation

@karpikpl
Copy link
Copy Markdown
Collaborator

Summary

Replaces PR #245 with a clean implementation on top of current main (v3.5.0). Activates OAuth provider support in nuxt-auth-utils — providers are enabled entirely by environment variables, no code changes required to switch.

What's new

Auth providers

  • GitHub OAuth — existing support updated with proper authorization ordering fix (see below)
  • Google OAuth — new /auth/google handler
  • Microsoft / Entra ID — new /auth/microsoft handler; setting NUXT_OAUTH_MICROSOFT_TENANT restricts to your org's AAD tenant automatically
  • Auth0 — new /auth/auth0 handler; acts as identity aggregator (GitHub, Google, LDAP, SAML, etc.)
  • Keycloak — new /auth/keycloak handler; self-hosted OIDC for air-gapped/regulated environments

Authorization utility

  • server/utils/authorization.ts — shared checkAuthorization() pure function + isUserAuthorized() Nuxt wrapper
  • Authorization runs before setUserSession() — unauthorized users never receive a session cookie (fixes ordering bug in original PR Support for Authentication Schemes - Decouple user auth from GitHub API credentials #245)
  • NUXT_AUTHORIZED_USERS — comma-separated logins/emails allowlist
  • NUXT_AUTHORIZED_EMAIL_DOMAINS — comma-separated domain allowlist

New env vars

Variable Purpose
NUXT_PUBLIC_REQUIRE_AUTH Replace NUXT_PUBLIC_USING_GITHUB_AUTH (backwards compat kept)
NUXT_PUBLIC_AUTH_PROVIDERS Comma-sep list: github, google, microsoft, auth0, keycloak
NUXT_AUTHORIZED_USERS App-level user allowlist
NUXT_AUTHORIZED_EMAIL_DOMAINS App-level domain allowlist

UI

  • PAT-mode info widget: When no OAuth is configured, a shield icon in the toolbar opens a dialog explaining available OAuth providers (with links to docs)
  • Dynamic login buttons: Sign-in page shows buttons only for configured providers

Documentation

  • DEPLOYMENT.md: Full Authentication section with step-by-step setup for all 5 providers, authorization reference, env var table
  • README.md: Updated auth env var docs, deprecated NUXT_PUBLIC_USING_GITHUB_AUTH, added provider variable reference table

Testing

  • 18 new unit tests for checkAuthorization() — all passing
  • Full test suite: 340 tests pass
  • Build: ✅ clean

Not included (deferred)

  • GitHub App JWT-based API credential decoupling (separate issue)

Closes #244
Supersedes #245

karpikpl and others added 5 commits April 23, 2026 11:50
- Add OAuth handlers for Google, Microsoft, Auth0, and Keycloak
- Add shared authorization utility with checkAuthorization() pure function
  and isUserAuthorized() Nuxt runtime config wrapper
- Authorization is checked BEFORE setUserSession() — unauthorized users
  never receive a session cookie
- Update GitHub OAuth handler with authorization check and login field
- Add NUXT_PUBLIC_REQUIRE_AUTH and NUXT_PUBLIC_AUTH_PROVIDERS env vars
- Add NUXT_AUTHORIZED_USERS and NUXT_AUTHORIZED_EMAIL_DOMAINS for
  optional application-level allowlisting (any provider)
- Keep NUXT_PUBLIC_USING_GITHUB_AUTH for backwards compatibility
- Add dynamic provider login buttons in MainComponent.vue
- Add PAT-mode shield icon button that opens dialog listing OAuth options
- Update nuxt.config.ts with runtimeConfig for all five providers
- Add 18 unit tests for authorization logic
- Update DEPLOYMENT.md: comprehensive Authentication section for all
  five providers (PAT, GitHub, Google, Microsoft, Auth0, Keycloak)
  with setup steps, env var reference table, and authorization docs
- Update README.md: deprecate NUXT_PUBLIC_USING_GITHUB_AUTH, document
  all new auth env vars with provider table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add server/modules/github-app-auth.ts: signs RS256 JWTs with Node.js
  built-in Web Crypto (no external deps), exchanges for installation token,
  caches with 5-min refresh buffer before expiry
- Update authentication.ts: GitHub App is now priority 2 in the credential
  chain (mock → App token → PAT → user OAuth session token)
- Add githubAppId/githubAppPrivateKey/githubAppInstallationId to nuxt.config.ts
  runtimeConfig
- Document new vars in .env, README.md, and DEPLOYMENT.md
- DEPLOYMENT.md: new 'GitHub App Installation Token' section with step-by-step
  App creation, private key export, and env var setup

This enables Google/Microsoft/Auth0/Keycloak users to access the dashboard
without any user-owned PAT — the installation token is machine-issued and
scoped to exactly the required org permissions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- github-app-auth.ts: replace single global cache with per-installation Map;
  add paginated listAppInstallations() with 5-min TTL + thundering-herd dedup;
  getGitHubAppToken() resolves org from githubOrg/githubEnt query param →
  config → single-install auto-select; no installation ID config needed
- server/api/installations.get.ts: new auth-protected endpoint returning
  accessible orgs (Flow 2: App JWT; Flow 1: session.organizations)
- server/routes/auth/github.get.ts: redirect to /select-org when multiple
  installations found (was always redirecting to first org)
- app/pages/select-org.vue: blocking org picker page; single org auto-
  navigates, multiple shows Vuetify v-select + Continue button
- app/router.options.ts: add /select-org route
- nuxt.config.ts: remove githubAppInstallationId (no longer needed)
- .env, README.md, DEPLOYMENT.md: update docs to reflect auto-discovery

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Auth

When a GitHub OAuth user logs in, call GET /user/installations with their
token so the org picker only shows orgs they have access to — not all 48
marketplace installations.

Priority order in /api/installations:
1. GitHub OAuth session token → /user/installations (user-filtered)
2. session.organizations (pre-populated at GitHub login)
3. App JWT → list ALL (fallback for non-GitHub OAuth / unauthenticated)

Also updated github.get.ts to always pre-populate session.organizations
(removed isPublicApp guard) so the picker page doesn't need a second
API call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…place apps

For private/internal apps (NUXT_PUBLIC_IS_PUBLIC_APP not set), listing all
installations via App JWT is correct — there are only a few known orgs.

Only marketplace/public apps need user-filtered results, since the App JWT
would otherwise return every org that ever installed the app from the marketplace.

When NUXT_PUBLIC_IS_PUBLIC_APP=true:
  1. GitHub OAuth token in session → /user/installations (live, user-scoped)
  2. session.organizations (pre-populated at GitHub login, used as fallback)

When NUXT_PUBLIC_IS_PUBLIC_APP=false (default):
  - App JWT lists all installations (private app, small known set)
  - Falls back to session.organizations for GitHub OAuth without App key

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

const key = createPrivateKey({ key: normalisePem(privateKeyPem), format: 'pem' })
const sign = createSign('RSA-SHA256')
sign.update(signingInput)
Comment thread server/modules/authentication.ts Outdated

// GitHub App installation token (preferred for decoupled auth — no PAT needed)
// Client ID can be used as the App ID per GitHub docs, so check either
const appId = config.githubAppId || config.oauth?.github?.clientId || ''
karpikpl and others added 7 commits April 23, 2026 15:03
…e apps

Public/marketplace apps (NUXT_PUBLIC_IS_PUBLIC_APP=true):
- App JWT lists ALL marketplace installs — useless for individual users
- Exception: GitHub OAuth login → /user/installations returns user-filtered list
- For all other login methods (Google, Microsoft, etc.): return empty list
  and show a manual text input so the user can type their org slug

Private/internal apps (default):
- App JWT lists a small, known set of installed orgs → show dropdown

This inverts the previous logic: the dropdown is for private apps where
the install list is meaningful; the text input is the fallback for public
apps where listing all installs would be misleading.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Without this check, any authenticated user (including Google/Microsoft
OAuth) could access Copilot metrics for ANY org that installed the
marketplace app by simply passing ?githubOrg=target-org.

Fix: when NUXT_PUBLIC_IS_PUBLIC_APP=true and no default org is
configured, the middleware verifies that the requested org is present
in session.organizations — a list populated at GitHub OAuth login via
GET /user/installations, which GitHub scopes to the user's own orgs.

Consequences:
- GitHub OAuth users: can only access orgs from their /user/installations
- Non-GitHub OAuth (Google, Microsoft, etc.): session.organizations is
  empty → 403 for all org-specific API requests. Users must sign in
  with GitHub to prove org membership, or an admin must set
  NUXT_PUBLIC_GITHUB_ORG to pin to a specific org.

The check is a no-op when NUXT_PUBLIC_IS_PUBLIC_APP is not set (private
internal apps) or when a default org is configured via env vars.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nstallations auth

When NUXT_PUBLIC_IS_PUBLIC_APP=true and NUXT_GITHUB_APP_PRIVATE_KEY is set,
throw a 500 error immediately. The two modes are fundamentally incompatible:
a private key grants app-level access to ALL marketplace installations, while
public apps should use the user's own GitHub OAuth token (naturally user-scoped).

Changes:
- installations.get.ts: throw 500 misconfiguration error instead of silently
  using the JWT path; remove try/catch that was swallowing errors
- middleware/github.ts: remove session.organizations authorization check — no
  longer needed since the incompatible config combo is blocked at the source
- github.get.ts: remove session.organizations storage — not needed for security,
  kept inline installations fetch only for redirect logic (single org → direct,
  multiple → /select-org)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When requireAuth/usingGithubAuth/isPublicApp is set and no session exists,
index.vue was unconditionally redirecting to /select-org before the user
logged in. /api/installations then returned 401.

Fix: only redirect to /select-org when auth is not required OR the user is
already logged in. When auth is required and there is no session, let
MainComponent render — it shows the login overlay, and after OAuth the
callback redirects to /select-org or directly to the org as before.

Also add a safety net in select-org.vue: if a 401 is returned (e.g. session
expired, direct URL navigation), redirect back to / so the login overlay
is displayed rather than showing a raw error message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- installations.get.ts: catch JWT/network errors and return [] so the UI
  shows a manual text input instead of an error page
- github-app-auth.ts: move installationsInflight = null into finally block
  so a failed promise is never cached (previously caused permanent failure
  until server restart)
- github-app-auth.ts: add diagnostic log in buildAppJwt showing whether a
  numeric App ID or Client ID is used as JWT issuer, with a clearer error
  message when PEM signing fails

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
useRouter() is auto-imported by Nuxt but must be called to get the
instance. Without this, all router.push/replace calls silently failed,
making the Continue button appear to do nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Default GitHub OAuth scope to ['read:user'] so the authorize URL
  is never built with an empty scope= param (GitHub returns 404)
- Remove /user/installations call from github.get.ts onSuccess handler;
  redirect all users to /select-org instead, which already uses the
  App JWT (private app) or user token (public app) correctly via
  /api/installations — fixes 404 for GitHub users not in the org
- Use config.oauth?.github?.clientId as JWT issuer fallback so
  NUXT_GITHUB_APP_ID is not required when NUXT_OAUTH_GITHUB_CLIENT_ID
  is already set

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
/** Build a short-lived JWT for GitHub App API calls. */
function buildAppJwt(appId: string, privateKey: string): string {
const now = Math.floor(Date.now() / 1000)
console.log(`[github-app-auth] Building JWT with App ID ${appId}`)
karpikpl and others added 3 commits April 23, 2026 21:33
NUXT_PUBLIC_IS_PUBLIC_APP=true now implies authentication is required,
consistent with NUXT_PUBLIC_USING_GITHUB_AUTH. Previously the server
middleware and MainComponent login overlay both missed isPublicApp in
their requireAuth checks, causing unauthenticated requests to fall
through and throw a generic 500 instead of redirecting to GitHub OAuth.

Changes:
- server/middleware/github.ts: add isPublicApp to requireAuth
- app/components/MainComponent.vue: add isPublicApp to isAuthRequired
  and to the activeProviders fallback (defaults to GitHub provider)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, setting NUXT_PUBLIC_AUTH_PROVIDERS=github alone was not
enough — you also needed NUXT_PUBLIC_REQUIRE_AUTH=true or the legacy
NUXT_PUBLIC_USING_GITHUB_AUTH=true to enforce authentication.

Now any non-empty AUTH_PROVIDERS value implies requireAuth, so the
minimum OAuth setup is:
  NUXT_PUBLIC_AUTH_PROVIDERS=github
  NUXT_OAUTH_GITHUB_CLIENT_ID=...
  NUXT_OAUTH_GITHUB_CLIENT_SECRET=...

NUXT_PUBLIC_USING_GITHUB_AUTH remains supported for backwards compat
but is no longer needed for new deployments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- NUXT_PUBLIC_AUTH_PROVIDERS being set now implies authentication required;
  NUXT_PUBLIC_REQUIRE_AUTH is no longer needed and hidden from docs
- NUXT_PUBLIC_USING_GITHUB_AUTH removed from docs (kept in code for compat)
- NUXT_PUBLIC_IS_PUBLIC_APP: org picker always shows text input (no server-
  side /user/installations enumeration); user types org slug directly
- .env.example: cleaned up auth section, one comment explains AUTH_PROVIDERS
- README + DEPLOYMENT: removed REQUIRE_AUTH and USING_GITHUB_AUTH entries,
  stripped REQUIRE_AUTH=true from all provider config examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@karpikpl karpikpl closed this Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Authentication Schemes

2 participants