Purpose: this document explains how to enable OpenID Connect (OIDC) login for the Fast Note Sync WebGUI. Read this when you want users to sign in to the WebGUI through an external identity provider such as Dex, Keycloak, or Casdoor. This does not cover MCP OAuth resource-server authorization; that is configured under oauth.
The oidc configuration enables SSO login for the WebGUI only.
When enabled:
- the WebGUI login page fetches
/api/user/auth/oidc/config; - if OIDC is enabled, the login page shows one OIDC login button for a single provider or one button per configured provider;
/api/user/auth/oidc/startcreates a state, nonce, and PKCE verifier, then redirects the browser to the selected provider;- the provider redirects back to its configured
redirect-url; - the service verifies the
id_token, maps the OIDC subject to a local FNS user, and issues the normal WebGUI login token.
For new WebGUI deployments, configure one or more providers under oidc.providers:
oidc:
enabled: true
callback-path: "/api/user/auth/oidc/callback"
auto-register: false
user-mapping:
subject-claim: "sub"
email-claim: "email"
username-claim: "preferred_username"
display-name-claim: "name"
providers:
- id: "dex"
display-name: "Login with Dex"
issuer: "https://dex.example.com/dex"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"
scopes:
- openid
- profile
- email
- id: "casdoor"
display-name: "Login with Casdoor"
issuer: "https://casdoor.example.com"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"
scopes:
- openid
- profile
- email
user-mapping:
display-name-claim: "displayName"
- id: "keycloak"
display-name: "Login with Keycloak"
issuer: "https://keycloak.example.com/realms/fns"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"Each provider has a stable id. Use lowercase letters, numbers, and hyphens, and keep it stable so provider selection and callback handling remain predictable.
Required for each provider:
issuerclient-idclient-secretredirect-url
Provider-level user-mapping overrides only the claims that differ from the global oidc.user-mapping. This is useful when most providers use name, but one provider uses a different display-name claim such as Casdoor displayName.
Defaults:
display-name:Login with OIDCcallback-path:/api/user/auth/oidc/callbackscopes:openid,profile,emailsubject-claim:subemail-claim:emailusername-claim:preferred_usernamedisplay-name-claim:name
Keep client-secret out of Git-managed public configuration.
Existing single-provider deployments can keep the historical top-level provider fields:
oidc:
enabled: true
display-name: "Login with SSO"
issuer: "https://idp.example.com"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"
callback-path: "/api/user/auth/oidc/callback"
scopes:
- openid
- profile
- email
auto-register: false
user-mapping:
subject-claim: "sub"
email-claim: "email"
username-claim: "preferred_username"
display-name-claim: "name"This form behaves like a single entry in oidc.providers. Prefer providers when you want multiple WebGUI login buttons.
FNS stores OIDC bindings in user_oidc_identity.
Login resolution order:
- If
(issuer, subject)is already bound, FNS logs in the bound local user. - If no binding exists and the OIDC email matches an existing local user, FNS creates the binding and logs in that user.
- If no user matches and
auto-register: true, FNS creates a local user and then creates the binding. - If no user matches and
auto-register: false, login fails.
For safer rollout, start with auto-register: false, create local users first, and let first login bind them by email.
When auto-register: true, the local username is generated from the first usable value in this order:
username-claimsuch aspreferred_usernamedisplay-name-claimsuch asname- the email local part before
@ oidc_plus the OIDC subject
The value is normalized to FNS username rules: letters, numbers, and underscores, 3 to 20 characters. If the username already exists, FNS appends a numeric suffix.
The WebGUI OIDC login uses standard OIDC discovery, authorization code flow, PKCE, and id_token verification. Google, Microsoft Entra ID, Auth0, Okta, Zitadel, and similar providers can work as long as they provide a normal OIDC issuer, client ID, client secret, redirect URL, and claims compatible with the configured user mapping.
GitHub is different: GitHub OAuth Apps are OAuth 2.0 providers and do not behave like a plain OIDC login provider with discovery and id_token in the same way. For GitHub login, usually put Dex, Keycloak, or Casdoor in front as an OIDC broker, or use a separate OAuth adapter that translates GitHub OAuth into the login flow you need.
Create a confidential client:
- Client ID:
fns-webgui - Client secret: same as the provider
client-secret - Redirect URI:
https://fns.example.com/api/user/auth/oidc/callback - Scopes:
openid,profile,email
Use the Dex issuer URL as the provider issuer, for example:
providers:
- id: "dex"
display-name: "Login with Dex"
issuer: "https://dex.example.com/dex"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"Create an OpenID Connect confidential client:
- Client ID:
fns-webgui - Client authentication: enabled
- Standard flow: enabled
- Valid redirect URI:
https://fns.example.com/api/user/auth/oidc/callback - PKCE:
S256is supported
Use the realm issuer as the provider issuer:
providers:
- id: "keycloak"
display-name: "Login with Keycloak"
issuer: "https://keycloak.example.com/realms/fns"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"Create or update an application:
- Redirect URI:
https://fns.example.com/api/user/auth/oidc/callback - Grant type:
authorization_code - Client ID and secret: match the provider
client-idandclient-secret - Scopes:
openid,profile,email
Use the Casdoor origin as the provider issuer:
providers:
- id: "casdoor"
display-name: "Login with Casdoor"
issuer: "https://casdoor.example.com"
client-id: "fns-webgui"
client-secret: "change-me"
redirect-url: "https://fns.example.com/api/user/auth/oidc/callback"Casdoor commonly uses displayName rather than name. If needed, set:
providers:
- id: "casdoor"
user-mapping:
display-name-claim: "displayName"redirect-url must be the externally reachable callback URL seen by the provider and the browser. Behind a reverse proxy, use the public HTTPS origin, not the internal container address.
Example:
redirect-url: "https://notes.example.com/api/user/auth/oidc/callback"If you run the WebGUI on a separate port, the callback still belongs to the API route. Configure the provider with the callback URL that reaches the FNS service.
The repository includes a Docker-backed smoke test:
scripts/oidc-smoke-test.shIt starts local Dex, Keycloak, and Casdoor instances and validates provider compatibility.
Regular tests do not start Docker:
go test ./...The provider smoke test uses a build tag internally:
go test -tags oidc_integration ./internal/oidc -run TestOIDCIntegrationProvideroidc provider discovery failed: verify the providerissuerand/.well-known/openid-configuration.OIDC state is invalid or expired: restart login; the callback was reused, expired, or generated by another service instance.OIDC token exchange failed: verify client ID, client secret, redirect URL, and PKCE support.- Login succeeds at the provider but fails in FNS: verify
emailandsubclaims, and check whetherauto-registershould be enabled. - The OIDC button is not shown: verify
oidc.enabled: trueand that the WebGUI can call/api/user/auth/oidc/configwithX-Client: WebGui.