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
- Applications → Create App Integration → OIDC, Web Application.
- Sign-in redirect URIs:
https://frem.sh/user/oauth2/<auth-source-name>/callback - Sign-out redirect URIs:
https://frem.sh/<your-org>/logout(optional but recommended) - Assignments: assign the application to the groups or users who should have fremforge access.
- Copy: Client ID, Client Secret, and the Okta domain (e.g.
https://acme.okta.com).- Issuer URL =
https://acme.okta.com(orhttps://acme.okta.com/oauth2/defaultfor custom auth servers)
- Issuer URL =
Microsoft Entra (Azure AD)
- App registrations → New registration, name “fremforge”, supported account types = single-tenant.
- Redirect URI: Web →
https://frem.sh/user/oauth2/<auth-source-name>/callback - Certificates & secrets → New client secret, copy the Value immediately (shown once).
- Overview: copy the Application (client) ID and Directory (tenant) ID.
- Issuer URL =
https://login.microsoftonline.com/<tenant-id>/v2.0
- Issuer URL =
- Token configuration → Add groups claim, select Security groups. This populates the
groupsclaim used for fremforge team mapping. - API permissions: confirm
openid,profile,emailare granted (they are by default).
Google Workspace
- Google Cloud Console → APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type: Web application.
- Authorised redirect URIs:
https://frem.sh/user/oauth2/<auth-source-name>/callback - Copy Client ID and Client Secret.
- Issuer URL =
https://accounts.google.com
- Issuer URL =
- In Google Workspace Admin → Security → API controls → Domain-wide delegation, restrict OAuth consent to your domain so only workspace members can authenticate.
Authentik
- Applications → Providers → Create → OAuth2/OpenID Connect Provider.
- Redirect URIs:
https://frem.sh/user/oauth2/<auth-source-name>/callback - Signing Key: select your Authentik signing certificate.
- 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>/). - Create an Application and bind it to the provider; set the launch URL to
frem.sh/user/login?redirect_to=/<your-org>.
Keycloak
- Your Realm → Clients → Create client.
- Client type: OpenID Connect. Client ID:
fremforge. - Valid redirect URIs:
https://frem.sh/user/oauth2/<auth-source-name>/callback - Credentials tab: copy the Client Secret.
- Issuer URL =
https://keycloak.acme.internal/realms/<your-realm>
- Issuer URL =
Auth0 / Okta Customer Identity Cloud
- Applications → Create Application → Regular Web Application.
- Allowed Callback URLs:
https://frem.sh/user/oauth2/<auth-source-name>/callback - Allowed Logout URLs:
https://frem.sh/<your-org>/logout - Settings: copy Domain, Client ID, Client Secret.
- Issuer URL =
https://<your-auth0-domain>/
- Issuer URL =
Step 3, register the provider in fremforge
- Org admin → SSO → Add auth source → OpenID Connect.
- 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-configurationto discover endpoints automatically.
- Attribute mapping (optional defaults work for most IdPs):
emailclaim → fremforge email (default:email)usernameclaim → Forgejo username (default:preferred_username→ falls back toemail)display_nameclaim → full name (default:name)
- 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
- User clicks Sign in with <name> at
frem.sh/user/login?redirect_to=/<your-org>. - fremforge redirects to your IdP’s authorisation endpoint with
scope=openid email profile. - User authenticates with the IdP (MFA enforced by the IdP, not fremforge).
- IdP redirects back to
frem.sh/user/oauth2/<auth-source-name>/callbackwith an authorisation code. - 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.
- 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:
- Configure OIDC so members can sign in via the IdP.
- Enable SCIM so the IdP pushes user/group changes automatically.
- 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:
- Generate a new secret in the IdP.
- Org admin → SSO → <auth source> → Edit → Client secret → paste the new value → Save.
- 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=loginagainst your IdP. The IdP MUST re-prompt for credentials and MFA; we verify the returned ID token’sauth_timeis within 5 minutes of now. Local-credential break-glass via/user/loginstill 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).
In your IdP, register a new OIDC application:
Field Value Application type Web (confidential) Grant types Authorization Code, with PKCE Redirect URI https://frem.sh/_app/<your-org>/_admin/-/stepup/callbackToken endpoint auth method client_secret_basicorclient_secret_postRequired scopes openid,emailauth_timeclaimRequired in ID token prompt=loginHonoured (default behaviour for most IdPs) 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
Verify: open
https://frem.sh/_app/<your-org>/_admin/billingin a fresh browser window. You should bounce through your IdP, get re-prompted, and land back at the billing page. The audit log shows anorg-session.step-up.okevent withauth_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_timeclaim. 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=loginat 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/_appform.
Cross-references
- SAML 2.0 SSO, use when your IdP team requires SAML
- SCIM 2.0 user provisioning, automate user lifecycle via your IdP’s SCIM client
- Security and supply chain, fremforge security model around SSO sessions
- Org admin, overview of all admin tabs