Skip to content

Latest commit

 

History

History
478 lines (344 loc) · 20.7 KB

File metadata and controls

478 lines (344 loc) · 20.7 KB

OAuth Provider Setup

Detailed setup instructions for each supported OAuth provider.

Built-in Providers vs Active Providers

Important distinction:

  • Built-in providers - Provider templates included in the OAuth plugin code (GitHub, Google, Azure, Auth0, Okta)

    • Zero runtime overhead - code presence ≠ execution
    • Not active until you configure them
    • No security risk from unused providers
  • Active providers - Providers you explicitly configure with credentials

    • Only configured providers are instantiated and available for authentication
    • Each requires clientId, clientSecret, and OAuth URLs
    • Only these providers accept login requests

Example: The OAuth plugin includes Okta code, but Okta authentication is not available unless you configure an Okta provider with credentials. Built-in providers are templates, not active endpoints.

Callback URLs: Both Sides Are Required

Every provider setup below has you register a callback URL with the provider. That is only half of it — you must also tell the plugin to send that URL, via the plugin-level redirectUri option:

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # e.g. https://yourdomain.com/oauth/callback
  providers:
    # ...
  • Set it once, at the plugin level (a sibling of providers, not inside a provider). The plugin appends the provider name per request: https://yourdomain.com/oauth/callback → https://yourdomain.com/oauth/github/callback, .../oauth/google/callback, and so on. That's why the value you configure has no provider name in it but the URL you register with the provider does.
  • If you omit it (or its environment variable is unset), the plugin refuses to start: a provider with no redirectUri (per-provider or plugin-level) is a configuration error raised at startup, naming the missing key. There is no default. (Earlier versions silently defaulted to http://localhost:9926/oauth/callback, which on a deployed app sent users to their own machine with no error raised anywhere.)
  • After deploying, verify what the plugin actually sends.

See Understanding Redirects for how this differs from postLoginRedirect.

GitHub OAuth

1. Create OAuth App

  1. Go to GitHub Settings > Developer settings > OAuth Apps
  2. Click "New OAuth App"
  3. Fill in the application details:
    • Application name: Your app name
    • Homepage URL: https://yourdomain.com
    • Authorization callback URL: https://yourdomain.com/oauth/github/callback
  4. Click "Register application"
  5. Copy the Client ID and generate a Client Secret

2. Configure Plugin

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # required on any deployed app — see Callback URLs above
  providers:
    github:
      clientId: ${OAUTH_GITHUB_CLIENT_ID}
      clientSecret: ${OAUTH_GITHUB_CLIENT_SECRET}
      scope: 'user:email' # Optional, default: 'user:email'

3. Environment Variables

export OAUTH_GITHUB_CLIENT_ID="your_client_id"
export OAUTH_GITHUB_CLIENT_SECRET="your_client_secret"
export OAUTH_REDIRECT_URI="https://yourdomain.com/oauth/callback"

Available Scopes

  • user - Access user profile data
  • user:email - Access user email addresses (default)
  • read:user - Read-only access to user profile

GitHub OAuth Scopes Documentation

Account adoption and the login claim

GitHub's default username claim is login (the GitHub handle, e.g. octocat). Because a login handle is not an email address, the plugin's account-adoption gate will deny any login whose username (login) matches an existing Harper account but whose email differs from it. This is by design — login handles are user-chosen and can be transferred or reassigned.

GitHub's verified email, however, is trusted when the /user/emails API call succeeds: the plugin fetches the primary email via the authenticated /user/emails API and marks it as github-authenticated provenance. If the fetch fails (network error, missing user:email scope), the provenance falls back to unauthenticated and adoption is denied. If you configure usernameClaim: 'email' (or use an onLogin hook to map the email to the account name), GitHub logins with a verified email can adopt an existing Harper account whose name matches that email.

Default behavior (login claim):

GitHub login Harper account Outcome
octocat octocat (exists) Denied — login claim is not the email
octocat (does not exist) Allowed — new session, no existing account

With usernameClaim: 'email' or an email-mapping hook:

GitHub verified email Harper account Outcome
alice@example.com alice@example.com (exists) Allowed — email is verified via GitHub's API
alice@example.com (does not exist) Allowed — new session

See Account-Adoption Gate for the full trust model.


Google OAuth (OIDC)

1. Create OAuth Client

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Go to APIs & Services > Credentials
  4. Click "Create Credentials" > "OAuth 2.0 Client ID"
  5. Configure OAuth consent screen if prompted
  6. Select "Web application" as application type
  7. Add authorized redirect URI: https://yourdomain.com/oauth/google/callback
  8. Copy the Client ID and Client Secret

2. Configure Plugin

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # required on any deployed app — see Callback URLs above
  providers:
    google:
      clientId: ${OAUTH_GOOGLE_CLIENT_ID}
      clientSecret: ${OAUTH_GOOGLE_CLIENT_SECRET}
      scope: 'openid profile email' # Optional, this is the default

3. Environment Variables

export OAUTH_GOOGLE_CLIENT_ID="your_client_id"
export OAUTH_GOOGLE_CLIENT_SECRET="your_client_secret"
export OAUTH_REDIRECT_URI="https://yourdomain.com/oauth/callback"

Available Scopes

  • openid - OpenID Connect (required)
  • profile - Access basic profile information
  • email - Access email address

Google OAuth Scopes Documentation


Azure AD (OIDC)

1. Register Application

  1. Go to Azure Portal
  2. Navigate to Azure Active Directory > App registrations
  3. Click "New registration"
  4. Fill in:
    • Name: Your application name
    • Supported account types: Choose appropriate option
    • Redirect URI: Web - https://yourdomain.com/oauth/azure/callback
  5. Click "Register"
  6. Copy the Application (client) ID and Directory (tenant) ID
  7. Go to Certificates & secrets > New client secret
  8. Copy the Client Secret value

2. Configure Plugin

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # required on any deployed app — see Callback URLs above
  providers:
    azure:
      clientId: ${OAUTH_AZURE_CLIENT_ID}
      clientSecret: ${OAUTH_AZURE_CLIENT_SECRET}
      tenantId: ${OAUTH_AZURE_TENANT_ID}
      scope: 'openid profile email' # Optional, this is the default

3. Environment Variables

export OAUTH_AZURE_CLIENT_ID="your_client_id"
export OAUTH_AZURE_CLIENT_SECRET="your_client_secret"
export OAUTH_AZURE_TENANT_ID="your_tenant_id"
export OAUTH_REDIRECT_URI="https://yourdomain.com/oauth/callback"

Available Scopes

  • openid - OpenID Connect (required)
  • profile - Access profile information
  • email - Access email address
  • User.Read - Read user profile

Microsoft Graph Permissions

Multi-Tenant Apps and Account Adoption

tenantId set to a real tenant GUID derives a fixed issuer, same as any other provider. Leaving tenantId unset (or set to common/organizations/consumers) accepts sign-ins from any tenant, but — because there's no single fixed issuer for those, and no key set exclusive to one tenant — ID tokens aren't trusted for account adoption: logins work, but every login is denied adoption (or quarantined), exactly as for any other unverified claim.

Pinning issuer alongside a multi-tenant tenantId does not keep the provider multi-tenant with per-login adoption gating. It turns the provider into a single-tenant one, for exactly the one tenant named by the pin — the plugin redirects verification to that tenant's own, non-shared signing keys and enforces that tenant's issuer, exactly as if you had set tenantId to that tenant's GUID directly. Sign-ins from every other tenant stop working outright: ID token verification fails, and the Microsoft Graph /v1.0/me fallback can't rescue the login either, since Graph returns mail, not email — the preset's default usernameClaim.

'@harperfast/oauth':
  providers:
    azure:
      tenantId: 'common' # only used to resolve the pin below to a real tenant
      issuer: 'https://login.microsoftonline.com/<your-tenant-guid>/v2.0' # provider becomes single-tenant: only this tenant signs in
      clientId: ${OAUTH_AZURE_CLIENT_ID}
      clientSecret: ${OAUTH_AZURE_CLIENT_SECRET}

If you need more than one tenant to sign in, configure a separate provider entry per tenant (each pinned to its own tenant GUID) rather than pinning one multi-tenant provider to an array of issuers — an array would let those tenants adopt each other's Harper accounts if they ever shared an email, so it's rejected outright.

userInfoUrl fetchEmail and the profile scope

If you override userInfoUrl away from Microsoft Graph, or set a custom scope, keep profile in scope whenever fetchEmail: true is set — the Graph UserInfo correlation needs the ID token's oid claim, which Azure only includes when profile was requested. The plugin warns at startup if this combination can't work.


Auth0 (OIDC)

1. Create Application

  1. Go to Auth0 Dashboard
  2. Navigate to Applications > Applications
  3. Click "Create Application"
  4. Choose "Regular Web Application"
  5. Go to Settings tab
  6. Copy Domain, Client ID, and Client Secret
  7. Add to Allowed Callback URLs: https://yourdomain.com/oauth/auth0/callback
  8. Add to Allowed Logout URLs: https://yourdomain.com (optional)
  9. Save changes

2. Configure Plugin

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # required on any deployed app — see Callback URLs above
  providers:
    auth0:
      domain: ${OAUTH_AUTH0_DOMAIN}
      clientId: ${OAUTH_AUTH0_CLIENT_ID}
      clientSecret: ${OAUTH_AUTH0_CLIENT_SECRET}
      scope: 'openid profile email' # Optional, this is the default

3. Environment Variables

export OAUTH_AUTH0_DOMAIN="yourapp.auth0.com"
export OAUTH_AUTH0_CLIENT_ID="your_client_id"
export OAUTH_AUTH0_CLIENT_SECRET="your_client_secret"
export OAUTH_REDIRECT_URI="https://yourdomain.com/oauth/callback"

Available Scopes

  • openid - OpenID Connect (required)
  • profile - Access profile information
  • email - Access email address

Auth0 Scopes Documentation


Okta (OIDC)

1. Create Application

  1. Go to Okta Developer Console
  2. Navigate to Applications > Applications
  3. Click "Create App Integration"
  4. Choose "OIDC - OpenID Connect"
  5. Select "Web Application"
  6. Fill in:
    • App integration name: Your application name
    • Sign-in redirect URIs: https://yourdomain.com/oauth/okta/callback
    • Sign-out redirect URIs: https://yourdomain.com (optional)
  7. Click "Save"
  8. Copy the Client ID and Client Secret
  9. Note your Okta domain (e.g., dev-12345.okta.com)

2. Configure Plugin

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # required on any deployed app — see Callback URLs above
  providers:
    okta:
      domain: ${OAUTH_OKTA_DOMAIN}
      clientId: ${OAUTH_OKTA_CLIENT_ID}
      clientSecret: ${OAUTH_OKTA_CLIENT_SECRET}
      scope: 'openid profile email groups' # Optional, this is the default

3. Environment Variables

export OAUTH_OKTA_DOMAIN="dev-12345.okta.com"
export OAUTH_OKTA_CLIENT_ID="your_client_id"
export OAUTH_OKTA_CLIENT_SECRET="your_client_secret"
export OAUTH_REDIRECT_URI="https://yourdomain.com/oauth/callback"

Available Scopes

  • openid - OpenID Connect (required)
  • profile - Access profile information
  • email - Access email address
  • groups - Access user's group memberships (for role mapping)

Group-Based Role Mapping

Okta supports mapping user groups to roles. The plugin will use the first group as the user's role:

'@harperfast/oauth':
  providers:
    okta:
      domain: ${OAUTH_OKTA_DOMAIN}
      clientId: ${OAUTH_OKTA_CLIENT_ID}
      clientSecret: ${OAUTH_OKTA_CLIENT_SECRET}
      scope: 'openid profile email groups'
      # First group will be used as role, falls back to defaultRole
      defaultRole: 'user'

To include groups in the ID token:

  1. In Okta Admin Console, go to Security > API > Authorization Servers
  2. Select your authorization server (or "default")
  3. Go to Claims tab
  4. Add a claim with:
    • Name: groups
    • Include in token type: ID Token, Always
    • Value type: Groups
    • Filter: Matches regex .* (or filter to specific groups)

Okta OAuth Documentation

Using a Custom Authorization Server

domain alone derives the org authorization server (/oauth2/v1/*), whose issuer is just https://{domain}. If you're using a custom authorization server instead (e.g. default, or a generated ID), set authServer alongside domain — the plugin derives both the path-inclusive endpoints and the issuer for you:

'@harperfast/oauth':
  providers:
    okta:
      domain: ${OAUTH_OKTA_DOMAIN}
      authServer: 'default' # or your custom auth server's ID
      clientId: ${OAUTH_OKTA_CLIENT_ID}
      clientSecret: ${OAUTH_OKTA_CLIENT_SECRET}

Without authServer, pointing authorizationUrl/tokenUrl/userInfoUrl/jwksUri at a custom authorization server directly (bypassing domain) no longer requires setting issuer explicitly — OIDC discovery derives it for you in the background instead. See issuer is usually unnecessary below.


Custom OIDC Provider

For other OpenID Connect compatible providers:

Configuration

'@harperfast/oauth':
  redirectUri: ${OAUTH_REDIRECT_URI} # required on any deployed app — see Callback URLs above
  providers:
    custom:
      clientId: ${OAUTH_CUSTOM_CLIENT_ID}
      clientSecret: ${OAUTH_CUSTOM_CLIENT_SECRET}
      authorizationUrl: ${OAUTH_CUSTOM_AUTHORIZATION_URL}
      tokenUrl: ${OAUTH_CUSTOM_TOKEN_URL}
      userInfoUrl: ${OAUTH_CUSTOM_USERINFO_URL}
      jwksUri: ${OAUTH_CUSTOM_JWKS_URL} # note: jwksUri, not jwksUrl
      scope: 'openid profile email'

Environment Variables

export OAUTH_REDIRECT_URI="https://yourdomain.com/oauth/callback"
export OAUTH_CUSTOM_CLIENT_ID="your_client_id"
export OAUTH_CUSTOM_CLIENT_SECRET="your_client_secret"
export OAUTH_CUSTOM_AUTHORIZATION_URL="https://provider.com/oauth/authorize"
export OAUTH_CUSTOM_TOKEN_URL="https://provider.com/oauth/token"
export OAUTH_CUSTOM_USERINFO_URL="https://provider.com/oauth/userinfo"
export OAUTH_CUSTOM_JWKS_URL="https://provider.com/.well-known/jwks.json"

issuer is usually unnecessary

When jwksUri is set but issuer isn't, the plugin derives it in the background via OIDC discovery: it probes .well-known/openid-configuration at your authorizationUrl's own origin and validates the result strictly against your configured endpoints before trusting it. This never blocks startup, and the first login that needs it waits only briefly for an in-flight attempt. On success, the server log names the exact value — copy it into issuer to pin it and skip discovery on future logins:

OIDC discovery for provider 'custom': issuer derived via discovery: https://provider.com/ — add issuer: https://provider.com/ to pin it.

Discovery only runs for an https:// authorizationUrl — a non-https one (and, separately, jwksUri with no usable issuer and no way to derive one) still fails fast at startup, since no amount of waiting fixes that. See Understanding issuer for what a missing/undiscoverable issuer means for account adoption.


Testing Your Configuration

  1. Start your Harper application
  2. Navigate to http://localhost:9926/oauth/{provider}/login
  3. Complete the OAuth flow
  4. Check your session for OAuth data

Verifying the Authorization Request

The login route is a 302, so you can inspect exactly what the plugin sends the provider without completing a login. Do this after every deployment:

curl -sS -D - -o /dev/null https://yourdomain.com/oauth/github/login | grep -i '^location'

In the returned Location header, confirm:

  • redirect_uri is your public origin — https%3A%2F%2Fyourdomain.com%2Foauth%2Fgithub%2Fcallback, not localhost
  • client_id is a real credential — not %24%7BOAUTH_..._CLIENT_ID%7D, which is a URL-encoded ${OAUTH_..._CLIENT_ID} and means the environment variable never reached the running app

Both failures happen before the provider is involved, so neither shows up in your provider's logs.

Common Issues

Missing redirectUri Fails to Start

Symptom: the app fails to start with an error naming a provider and redirectUri.

Cause: redirectUri isn't set at the plugin level or on that provider. There is no localhost fallback — a missing redirectUri used to default to http://localhost:9926/oauth/callback and reach the provider's consent screen (rather than failing with redirect_uri_mismatch) whenever localhost was also registered with the provider, so login just silently never completed. That's now a startup error instead.

Solution: set the plugin-level redirectUri to your public origin, as described in Callback URLs. Registering the URL with your provider does not affect what the plugin sends.

Provider Rejects an Unexpanded ${OAUTH_...} Value

Symptom: the provider errors immediately with invalid_client / "OAuth client was not found", and the authorization URL contains client_id=%24%7BOAUTH_GITHUB_CLIENT_ID%7D.

Cause: ${VAR} placeholders in config.yaml are substituted from the environment of the running app. When a variable is undefined, the placeholder is passed through as a literal string rather than raising a configuration error — so the plugin starts up looking healthy and ships ${OAUTH_GITHUB_CLIENT_ID} to the provider verbatim.

Solution: make the variable available to the deployed app, not just your shell — for Harper Fabric, see managing runtime environment variables. Then re-check with the curl above.

Redirect URI Mismatch

Error: redirect_uri_mismatch or similar

Solution: the URL the plugin sends and the URL registered with the provider must match exactly. Check both sides:

  • Plugin: redirectUri is set to your public origin plus /oauth/callback (the plugin appends the provider name itself)
  • Provider: the registered callback is the provider-specific form:
https://yourdomain.com/oauth/{provider}/callback

Watch for a trailing slash, http vs https, and www. differences — providers compare byte-for-byte.

Invalid Client Credentials

Error: invalid_client or unauthorized_client

Solution:

  • Verify client ID and secret are correct
  • Check environment variables are set in the deployed app's environment, and that they expanded (see above)
  • Ensure client secret hasn't expired

Missing Email Address

Error: User email not available

Solution:

  • Verify email scope is requested
  • Check provider consent screen configuration
  • Ensure user has granted email permission

Next Steps