Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →

OIDC single sign-on

fremforge supports OpenID Connect (OIDC) for single sign-on. OIDC is the recommended protocol for most organisations. Every major IdP (Entra, Okta, Google Workspace, Auth0, Authentik, Keycloak) ships OIDC natively. The signing-key lifecycle is automatic via the OIDC discovery and JWKS endpoints, and the diagnostic surface is smaller than SAML.

Use SAML only when your IdP team specifically requires it (legacy federation contracts, regulated-industry interop, or IdPs that pre-date OIDC). Both protocols land at the same Forgejo session and offer equivalent security. OIDC has less operational overhead over time.

Setup

Step 1, verify a domain

Org admin → SSO → Verified domains → add your email domain (e.g. acme.com). Prove control via one of:

  • DNS TXT record: add _fremforge-verification=<token> to your domain’s DNS, then click Verify.
  • HTTP file: serve the token at https://acme.com/.well-known/fremforge-verification, then click Verify.

Domain verification is required before registering any auth source. One verification covers both OIDC and SAML. You only do this once.

Step 2, create an OAuth 2.0 application in your IdP

fremforge needs three values from your IdP: Client ID, Client Secret, and the Issuer URL (also called the OIDC discovery URL root). Configure your IdP as follows.

Okta

  1. Applications → Create App Integration → OIDC, Web Application.
  2. Sign-in redirect URIs: https://frem.sh/user/oauth2/<auth-source-name>/callback
  3. Sign-out redirect URIs: https://frem.sh/<your-org>/logout (optional but recommended)
  4. Assignments: assign the application to the groups or users who should have fremforge access.
  5. Copy: Client ID, Client Secret, and the Okta domain (e.g. https://acme.okta.com).
    • Issuer URL = https://acme.okta.com (or https://acme.okta.com/oauth2/default for custom auth servers)

Microsoft Entra (Azure AD)

  1. App registrations → New registration, name “fremforge”, supported account types = single-tenant.
  2. Redirect URI: Web → https://frem.sh/user/oauth2/<auth-source-name>/callback
  3. Certificates & secrets → New client secret, copy the Value immediately (shown once).
  4. Overview: copy the Application (client) ID and Directory (tenant) ID.
    • Issuer URL = https://login.microsoftonline.com/<tenant-id>/v2.0
  5. Token configuration → Add groups claim, select Security groups. This populates the groups claim used for fremforge team mapping.
  6. API permissions: confirm openid, profile, email are granted (they are by default).

Google Workspace

  1. Google Cloud Console → APIs & Services → Credentials → Create credentials → OAuth client ID.
  2. Application type: Web application.
  3. Authorised redirect URIs: https://frem.sh/user/oauth2/<auth-source-name>/callback
  4. Copy Client ID and Client Secret.
    • Issuer URL = https://accounts.google.com
  5. In Google Workspace Admin → Security → API controls → Domain-wide delegation, restrict OAuth consent to your domain so only workspace members can authenticate.

Authentik

  1. Applications → Providers → Create → OAuth2/OpenID Connect Provider.
  2. Redirect URIs: https://frem.sh/user/oauth2/<auth-source-name>/callback
  3. Signing Key: select your Authentik signing certificate.
  4. Copy Client ID, Client Secret, and the OpenID Configuration URL (strip /.well-known/openid-configuration, the issuer URL is the root, e.g. https://sso.acme.internal/application/o/<slug>/).
  5. Create an Application and bind it to the provider; set the launch URL to frem.sh/user/login?redirect_to=/<your-org>.

Keycloak

  1. Your Realm → Clients → Create client.
  2. Client type: OpenID Connect. Client ID: fremforge.
  3. Valid redirect URIs: https://frem.sh/user/oauth2/<auth-source-name>/callback
  4. Credentials tab: copy the Client Secret.
    • Issuer URL = https://keycloak.acme.internal/realms/<your-realm>

Auth0 / Okta Customer Identity Cloud

  1. Applications → Create Application → Regular Web Application.
  2. Allowed Callback URLs: https://frem.sh/user/oauth2/<auth-source-name>/callback
  3. Allowed Logout URLs: https://frem.sh/<your-org>/logout
  4. Settings: copy Domain, Client ID, Client Secret.
    • Issuer URL = https://<your-auth0-domain>/

Step 3, register the provider in fremforge

  1. Org admin → SSO → Add auth source → OpenID Connect.
  2. Fill in:
    • Display name, shown on the login page button, e.g. “Sign in with Okta”.
    • Client ID and Client Secret from Step 2.
    • Issuer URL, fremforge fetches <issuer>/.well-known/openid-configuration to discover endpoints automatically.
  3. Attribute mapping (optional defaults work for most IdPs):
    • email claim → fremforge email (default: email)
    • username claim → Forgejo username (default: preferred_username → falls back to email)
    • display_name claim → full name (default: name)
  4. Save, fremforge performs a discovery fetch and validates connectivity before persisting.

Once saved, a Sign in with <name> button appears on your org’s login page at frem.sh/user/login?redirect_to=/<your-org>.

What happens at login

  1. User clicks Sign in with <name> at frem.sh/user/login?redirect_to=/<your-org>.
  2. fremforge redirects to your IdP’s authorisation endpoint with scope=openid email profile.
  3. User authenticates with the IdP (MFA enforced by the IdP, not fremforge).
  4. IdP redirects back to frem.sh/user/oauth2/<auth-source-name>/callback with an authorisation code.
  5. fremforge exchanges the code for tokens, validates the ID token signature against the IdP’s JWKS, extracts the email and username claims, and creates or updates the Forgejo user.
  6. If Require SSO is enabled (Org admin → SSO → Policies), direct password login is blocked. Only the IdP path works.

Require SSO

Org admin → SSO → Policies → Require SSO for all members, after enabling this:

  • Existing members must re-authenticate via the IdP on next login.
  • PATs and SSH keys remain valid as authentication mechanisms, but with Require SSO on, the IdP session is revalidated every 15 minutes on Git operations. A user deactivated in the IdP therefore loses push/pull access within 15 minutes even if their SSH key is still registered. No separate key revocation is required.
  • New member invitations land as SSO-only accounts.

Pair with SCIM for full lifecycle management

OIDC handles authentication. To automate user provisioning and deprovisioning (create accounts on hire, deactivate on termination, sync group memberships), pair OIDC with SCIM 2.0 provisioning.

Most enterprise IdPs (Okta, Entra) support both OIDC and SCIM simultaneously. The typical setup:

  1. Configure OIDC so members can sign in via the IdP.
  2. Enable SCIM so the IdP pushes user/group changes automatically.
  3. Enable Require SSO. With SCIM active, deprovisioning the user in the IdP immediately blocks their fremforge access.

Token rotation

fremforge caches the IdP’s JWKS (signing keys) and refreshes on a schedule and on signature validation failure. Key rotation on the IdP side (e.g. Okta automatic key rotation) is handled transparently. No manual action required.

If you rotate the Client Secret in the IdP:

  1. Generate a new secret in the IdP.
  2. Org admin → SSO → <auth source> → Edit → Client secret → paste the new value → Save.
  3. Old secret can be revoked in the IdP immediately after saving.

Disabling OIDC

Org admin → SSO → <auth source> → Disable. Users who have only the OIDC path fall back to email + password (if a local password is set) or become login-blocked until re-enabled. To prevent orphaned accounts, disable SSO before offboarding the IdP application, not after.

Troubleshooting

Redirect URI mismatch

The IdP rejects the callback if the redirect URI registered in the IdP doesn’t exactly match https://frem.sh/user/oauth2/<auth-source-name>/callback. The <auth-source-name> is the Name you set when adding the auth source in fremforge (e.g. okta-primary, entra-prod), it’s used by Forgejo’s OAuth2 router to dispatch the callback. Copy the exact callback URL from Org admin → SSO → <auth source> → Details rather than typing it manually; URL is case-sensitive and trailing slashes matter.

invalid_client on token exchange

Client ID or Client Secret is wrong. Re-copy from the IdP console. Some IdPs show the secret only once at creation time. Generate a new secret if the original was not captured.

Claims missing (username or email blank)

Check the IdP’s attribute/claim release policy. fremforge requires at minimum the email claim in the ID token. If your IdP gates claim release on explicit permissions (e.g. Entra requires profile and email API permissions), ensure they are granted and consented.

Users provisioned but can’t log in

If Require SSO is on and the user’s email domain doesn’t match a verified domain, fremforge blocks the login. Verify the domain under Org admin → SSO → Verified domains. The domain in the user’s email claim must match.

Per-org session-binding step-up (forced IdP re-auth)

Beyond the standard SSO login, fremforge can force a fresh IdP-attested authentication every time a user crosses into a new org’s admin surface. This is the regulated-sector strength of “per-org session binding” promised on the pricing page, useful when each cross-org admin action needs to carry an IdP-attested authentication scoped to that specific org.

Two strength tiers, both default-on:

  • Soft binding (default if you don’t wire step-up): the bounce on first cross-org access goes through Forgejo’s universal /user/login. If your Forgejo session is fresh, the round-trip is invisible. Local-credential break-glass remains intact.
  • Hard binding (wire below): the bounce goes through fremforge’s step-up endpoint, which initiates an OIDC AuthN with prompt=login against your IdP. The IdP MUST re-prompt for credentials and MFA; we verify the returned ID token’s auth_time is within 5 minutes of now. Local-credential break-glass via /user/login still works for org owners, hard binding only changes the cross-org-access path, not the universal login.

Wiring hard binding

Hard binding requires a SECOND OIDC client in your IdP next to the one Forgejo uses (same issuer, separate client_id + client_secret, separate redirect URI).

  1. In your IdP, register a new OIDC application:

    FieldValue
    Application typeWeb (confidential)
    Grant typesAuthorization Code, with PKCE
    Redirect URIhttps://frem.sh/_app/<your-org>/_admin/-/stepup/callback
    Token endpoint auth methodclient_secret_basic or client_secret_post
    Required scopesopenid, email
    auth_time claimRequired in ID token
    prompt=loginHonoured (default behaviour for most IdPs)
  2. In fremforge, go to Org admin → SSO, expand Add OIDC auth source, and fill in the optional Per-org session-binding step-up fields at the bottom of the form:

    • Step-up Client ID: from your IdP’s new app
    • Step-up Client Secret: from your IdP’s new app, encrypted at rest with AES-256-GCM (HKDF-derived subkey) before persistence
    • Step-up Redirect URI: https://frem.sh/_app/<your-org>/_admin/-/stepup/callback
  3. Verify: open https://frem.sh/_app/<your-org>/_admin/billing in a fresh browser window. You should bounce through your IdP, get re-prompted, and land back at the billing page. The audit log shows an org-session.step-up.ok event with auth_time.

If the step-up fields are left blank, the auth source falls back to soft binding, useful for orgs that want SSO at login but don’t need per-org IdP attestation.

Troubleshooting hard binding

  • “IdP did not emit auth_time”: the IdP returned an ID token without the auth_time claim. fremforge requires it because that’s how we verify the freshness of the re-authentication. Check the IdP’s claim policy, most have a toggle for “include auth_time”.
  • “auth_time too old”: the IdP honoured prompt=login at protocol level but didn’t actually re-prompt the user. Tighten the IdP’s “session lifetime” or “max age” policy on this client.
  • “step-up cookie binding mismatch”: the user changed identity mid-flow (e.g. opened a second browser tab and authenticated as a different user). Restart from /<your-org>/_admin/billing.
  • “step-up redirect URI rejected”: the URI failed our public-HTTPS guard (private/loopback/metadata addresses are blocked). Use the canonical frem.sh/_app form.

Cross-references