Resolves #306. SSO sign-in via OIDC (Authentik / Keycloak / etc.) now links the SSO identity to an existing email/password admin instead of bouncing to /login with `?error=UNKNOWN`. Account-linking is gated on the operator-supplied **Domain** field — cross-domain claims from a compromised IdP are refused. Also bundles the deprecated `oidcProvider` → `@better-auth/oauth-provider` migration. **Operators using the OAuth-provider feature must rotate registered client secrets after upgrade** (legacy plaintext → hashed storage; see the 0012 migration notes). Verified end-to-end on the pr-307 image against a real Authentik instance: SSO login lands on the dashboard, `accounts` table gets both `credential` and `authentik` rows for the same user. See PR description for full details.
16 KiB
SSO and OIDC Setup Guide
This guide explains how to configure Single Sign-On (SSO) and OpenID Connect (OIDC) provider functionality in Gitea Mirror.
Overview
Gitea Mirror supports four authentication methods:
- Email & Password - Traditional authentication (always enabled)
- SSO (Single Sign-On) - Allow users to authenticate using external OIDC providers
- Header / Forward Authentication - Trust pre-authenticated requests from a reverse proxy (Authentik, Authelia, oauth2-proxy, etc.)
- OIDC Provider - Allow other applications to authenticate users through Gitea Mirror
Configuration
SSO and OIDC Provider settings are managed through the web UI in the Configuration page under the "Authentication" tab. Header / Forward Authentication is configured via environment variables only — see Setting up Header / Forward Authentication below.
Setting up SSO (Single Sign-On)
SSO allows your users to sign in using external identity providers like Google, Okta, Azure AD, etc.
Adding an SSO Provider
- Navigate to Configuration → Authentication → SSO Providers
- Click "Add Provider"
- Fill in the provider details:
Required Fields
- Issuer URL: The OIDC issuer URL (e.g.,
https://accounts.google.com) - Domain: The email domain this provider serves (e.g.,
example.com). This is load-bearing for account-linking trust — see Account Linking below. Multi-domain IdPs can use a comma-separated list (example.com,subsidiary.com). - Provider ID: A unique identifier for this provider (e.g.,
google-sso) - Client ID: The OAuth client ID from your provider
- Client Secret: The OAuth client secret from your provider
Auto-Discovery
If your provider supports OIDC discovery, you can:
- Enter the Issuer URL
- Click "Discover"
- The system will automatically fetch the authorization and token endpoints
Manual Configuration
For providers without discovery support, manually enter:
- Authorization Endpoint: The OAuth authorization URL
- Token Endpoint: The OAuth token exchange URL
- JWKS Endpoint: The JSON Web Key Set URL (optional)
- UserInfo Endpoint: The user information endpoint (optional)
Redirect URL
When configuring your SSO provider, use this redirect URL:
https://your-domain.com/api/auth/sso/callback/{provider-id}
Replace {provider-id} with your chosen Provider ID.
Account Linking
When a user signs in via SSO whose email matches an existing email/password account, Gitea Mirror will try to link the SSO identity to that account so the user lands on the same dashboard — instead of being bounced to the login page or creating a duplicate account.
The trust model is domain-scoped:
- Every SSO provider you register is automatically marked
domainVerified: truefor the Domain field you set on registration. - An SSO sign-in is auto-linked to an existing local account only when the user's email address actually belongs to that registered domain. Cross-domain emails are rejected even if the provider is registered.
- Local email/password accounts in this app are never verified (no email-verification flow exists), so we deliberately turn off
requireLocalEmailVerifiedon the linker — otherwise no one could ever link.
What this means for you:
- Set the Domain field to the email domain(s) your IdP actually issues identities for. Don't set it to
example.comif your IdP issues@elsewhere.comemails — auto-linking will silently refuse them. - If your IdP allows users to self-register or claim arbitrary emails within the registered domain, an attacker on that IdP can absorb a local account by claiming the same email. This is a trust decision: by registering an IdP, you're vouching for its identity model for that domain.
- Multi-domain IdPs (one Authentik serving
acme.com,acquired.io) work fine — list every domain comma-separated in the Domain field.
Example: Google SSO Setup
- Go to Google Cloud Console
- Create a new OAuth 2.0 Client ID
- Add authorized redirect URI:
https://your-domain.com/api/auth/sso/callback/google-sso - In Gitea Mirror:
- Issuer URL:
https://accounts.google.com - Domain:
your-company.com - Provider ID:
google-sso - Client ID: [Your Google Client ID]
- Client Secret: [Your Google Client Secret]
- Click "Discover" to auto-fill endpoints
- Issuer URL:
Example: Okta SSO Setup
- In Okta Admin Console, create a new OIDC Web Application
- Set redirect URI:
https://your-domain.com/api/auth/sso/callback/okta-sso - In Gitea Mirror:
- Issuer URL:
https://your-okta-domain.okta.com - Domain:
your-company.com - Provider ID:
okta-sso - Client ID: [Your Okta Client ID]
- Client Secret: [Your Okta Client Secret]
- Click "Discover" to auto-fill endpoints
- Issuer URL:
Example: Authentik SSO Setup
Working Authentik deployments (see #134) follow these steps:
- In Authentik, create a new Application and OIDC Provider (implicit flow works well for testing).
- Start creating an SSO provider inside Gitea Mirror so you can copy the redirect URL shown (
https://your-domain.com/api/auth/sso/callback/authentikif you pickauthentikas your Provider ID). - Paste that redirect URL into the Authentik Provider configuration and finish creating the provider.
- Copy the Authentik issuer URL, client ID, and client secret.
- Back in Gitea Mirror:
- Issuer URL: the exact value from Authentik (keep any trailing slash Authentik shows).
- Provider ID: match the one you used in step 2.
- Click Discover so Gitea Mirror stores the authorization, token, and JWKS endpoints (Authentik publishes them via discovery).
- Domain: enter the email domain you expect to match (e.g.
example.com).
- Save the provider and test the login flow.
Notes:
- Make sure
BETTER_AUTH_URLand (if you serve the UI from multiple origins)BETTER_AUTH_TRUSTED_ORIGINSpoint at the public URL users reach. A mismatch can surface as 500 errors after redirect. - Set the Domain field to the email domain your Authentik users actually have. Auto-linking to an existing local admin only happens for emails in that domain — see Account Linking for the trust model. (Authentik's default email scope mapping returns
email_verified: Falsefor OIDC clients, which is why account linking is gated on the domain match here rather than the IdP's verified-email claim.) - If you created an Authentik provider before v3.8.10 you should delete it and re-add it after upgrading; older versions saved incomplete endpoint data which leads to the
url.startsWitherror explained in the Troubleshooting section.
Setting up Header / Forward Authentication
Header authentication trusts a reverse proxy (Authentik, Authelia, oauth2-proxy, Traefik forward-auth, etc.) to authenticate users upstream and pass identity in HTTP headers. When enabled, Gitea Mirror reads the configured headers on each request and resolves the user automatically — no login form, no callback.
Important — operator-controlled by design. Header auth is configured via environment variables only, not the UI. This is intentional: trusting a header means trusting whatever upstream sets it, and that decision belongs to the operator who controls the reverse proxy, not to a logged-in user inside the app. Make sure your reverse proxy strips these headers from inbound client requests, otherwise anyone can spoof them.
Environment variables
| Variable | Description | Default |
|---|---|---|
HEADER_AUTH_ENABLED |
Master switch — must be true to enable |
false |
HEADER_AUTH_USER_HEADER |
Header containing the username | X-Authentik-Username |
HEADER_AUTH_EMAIL_HEADER |
Header containing the email address | X-Authentik-Email |
HEADER_AUTH_NAME_HEADER |
Header containing the display name | X-Authentik-Name |
HEADER_AUTH_AUTO_PROVISION |
If true, create a new user when an unknown username arrives. If false, unknown users are rejected. |
false |
HEADER_AUTH_ALLOWED_DOMAINS |
Comma-separated email domain allowlist. Empty = allow any. | (empty) |
See also: docs/ENVIRONMENT_VARIABLES.md.
Example: Docker Compose with Authentik
services:
gitea-mirror:
image: ghcr.io/raylabshq/gitea-mirror:latest
environment:
HEADER_AUTH_ENABLED: "true"
HEADER_AUTH_USER_HEADER: "X-Authentik-Username"
HEADER_AUTH_EMAIL_HEADER: "X-Authentik-Email"
HEADER_AUTH_NAME_HEADER: "X-Authentik-Name"
HEADER_AUTH_AUTO_PROVISION: "true"
HEADER_AUTH_ALLOWED_DOMAINS: "example.com,example.org"
The defaults are Authentik-shaped, so for an Authentik proxy provider you generally only need to set HEADER_AUTH_ENABLED=true (and HEADER_AUTH_AUTO_PROVISION=true if you want first-login self-registration).
Example: Authelia
Authelia uses different header names — override them:
environment:
HEADER_AUTH_ENABLED: "true"
HEADER_AUTH_USER_HEADER: "Remote-User"
HEADER_AUTH_EMAIL_HEADER: "Remote-Email"
HEADER_AUTH_NAME_HEADER: "Remote-Name"
HEADER_AUTH_AUTO_PROVISION: "true"
Then configure Authelia's authz rules to protect the gitea-mirror route and inject the Remote-* headers.
Behaviour and lookup order
- The middleware checks for a cookie session first. Header auth only fires when there is no existing session, so users who logged in via password or SSO are not affected.
- Lookup is by
HEADER_AUTH_USER_HEADERvalue matched againstusers.username; if no match andHEADER_AUTH_EMAIL_HEADERis set, a second lookup is tried againstusers.email. - If neither matches:
- With
HEADER_AUTH_AUTO_PROVISION=true, a new user row is created (email defaults to<username>@header-auth.localif no email header is present). - With
HEADER_AUTH_AUTO_PROVISION=false, the request is rejected with a warning logged.
- With
- If
HEADER_AUTH_ALLOWED_DOMAINSis non-empty and the email header arrives, the email's domain must be in the list or the request is rejected.
Verifying it's enabled
When header auth is active, the Authentication settings page renders a green "Header Authentication / Via reverse proxy" badge under the auth-methods status block. You can also probe /api/auth/header-status, which returns { "enabled": true, ... } once the env vars are set.
Security checklist
- Strip the headers at your edge. Your reverse proxy must remove inbound
HEADER_AUTH_USER_HEADER/ email / name headers from client requests before re-injecting its own. Otherwise any unauthenticated client can setX-Authentik-Username: adminand walk in. - Bind the app to the proxy only. Don't expose Gitea Mirror's port directly to the network when header auth is on; only the trusted proxy should reach it.
- Use HTTPS between proxy and clients. Headers travel in plaintext on the link they cross.
- Be conservative with
HEADER_AUTH_AUTO_PROVISION. With it off, you provision users in the UI/DB once and the proxy fills in sessions thereafter — safer for shared deployments.
Setting up OIDC Provider
The OIDC Provider feature allows other applications to use Gitea Mirror as their authentication provider.
Creating OAuth Applications
-
Navigate to Configuration → Authentication → OAuth Applications
-
Click "Create Application"
-
Fill in the application details:
- Application Name: Display name for the application
- Application Type: Web, Mobile, or Desktop
- Redirect URLs: One or more redirect URLs (one per line)
-
After creation, you'll receive:
- Client ID: Share this with the application
- Client Secret: Keep this secure and share only once
OIDC Endpoints
Applications can use these standard OIDC endpoints:
- Discovery:
https://your-domain.com/.well-known/openid-configuration - Authorization:
https://your-domain.com/api/auth/oauth2/authorize - Token:
https://your-domain.com/api/auth/oauth2/token - UserInfo:
https://your-domain.com/api/auth/oauth2/userinfo - JWKS:
https://your-domain.com/api/auth/jwks
Supported Scopes
openid- Required, provides user IDprofile- User's name, username, and profile pictureemail- User's email address and verification status
Example: Configuring Another Application
For an application to use Gitea Mirror as its OIDC provider:
// Example configuration for another app
const oidcConfig = {
issuer: 'https://gitea-mirror.example.com',
clientId: 'client_xxxxxxxxxxxxx',
clientSecret: 'secret_xxxxxxxxxxxxx',
redirectUri: 'https://myapp.com/auth/callback',
scope: 'openid profile email'
};
User Experience
Logging In with SSO
When SSO is configured:
- Users see tabs for "Email" and "SSO" on the login page
- In the SSO tab, they can:
- Click a specific provider button (if configured)
- Enter their work email to be redirected to the appropriate provider
OAuth Consent Flow
When an application requests authentication:
- Users are redirected to Gitea Mirror
- If not logged in, they authenticate first
- They see a consent screen showing:
- Application name
- Requested permissions
- Option to approve or deny
Security Considerations
- Client Secrets: Store OAuth client secrets securely
- Redirect URLs: Only add trusted redirect URLs for applications
- Scopes: Applications only receive the data for approved scopes
- Token Security: Access tokens expire and can be revoked
Troubleshooting
SSO Login Issues
- "Invalid origin" error: Check that your Gitea Mirror URL matches the configured redirect URI
- "Provider not found" error: Ensure the provider is properly configured and enabled
- Redirect loop: Verify the redirect URI in both Gitea Mirror and the SSO provider match exactly
?error=UNKNOWNon the homepage after a successful upstream login (or?error=account%20not%20linkedin development): the SSO callback succeeded but Better Auth refused to link the SSO identity to an existing local account. The most common cause is the SSO provider's registered Domain not matching the user's actual email domain — see Account Linking. SetBETTER_AUTH_LOG_LEVEL=debugto see Better Auth's full callback trace (look for "User already exist but account isn't linked to ...") and confirm the diagnosis. The same symptom in production gets sanitized toUNKNOWNby Better Auth's error page before the redirect, which is why the visible error is opaque.TypeError: undefined is not an object (evaluating 'url.startsWith'): This indicates the stored provider configuration is missing OIDC endpoints. Delete the provider from Gitea Mirror and re-register it using the Discover button so authorization/token URLs are saved (see #73 and #122 for examples).
OIDC Provider Issues
- Application not found: Ensure the client ID is correct
- Invalid redirect URI: The redirect URI must match exactly what's configured
- Consent not working: Check browser cookies are enabled
Managing Access
Revoking SSO Access
Currently, SSO sessions are managed through the identity provider. To revoke access:
- Log out of Gitea Mirror
- Revoke access in your identity provider's settings
Disabling OAuth Applications
To disable an application:
- Go to Configuration → Authentication → OAuth Applications
- Find the application
- Click the delete button
This immediately prevents the application from authenticating new users.
Best Practices
- Use HTTPS: Always use HTTPS in production for security
- Regular Audits: Periodically review configured SSO providers and OAuth applications
- Principle of Least Privilege: Only grant necessary scopes to applications
- Monitor Usage: Keep track of which applications are accessing your OIDC provider
- Secure Storage: Store client secrets in a secure location, never in code
Migration Notes
If migrating from the previous JWT-based authentication:
- Existing users remain unaffected
- Users can continue using email/password authentication
- SSO can be added as an additional authentication method