Single Sign-On (SSO) Configuration

Adapted from docs/admin/SingleSignOn.md in the Wikantik repository; current as of release 2.4.58.

Wikantik supports Single Sign-On via pac4j as an OpenID Connect (OIDC) and/or SAML 2.0 service provider. This is the authoritative operator reference for configuring SSO. The implementation lives in wikantik-main/src/main/java/com/wikantik/auth/sso/ (SSOConfig, SSOLoginModule, SSOCallbackServlet, SSORedirectServlet, SSOAutoProvisionService). The historical design notes in docs/archive/OAuthImplementation.md and docs/archive/FullOAuth.md are superseded and kept only for context.

SSORedirectServlet (/sso/login) is the entry point of the SSO flow. It locates the appropriate pac4j client (selected by an optional client_name query parameter; falls back to the first configured client), requests a redirection action from it, and sends the browser to the Identity Provider. If SSO is disabled or unconfigured it responds 404; configuration errors respond 500; no matching client redirects to /login?error=no_sso_client; any other failure redirects to /login?error=sso_redirect_failed. SSOCallbackServlet (/sso/callback) receives the IdP response; a failure there redirects to /login?error=sso_callback_failed.

SSO coexists with traditional username/password login — it is purely additive. When enabled, the SPA login page shows a provider button alongside the local login form.

Related — IdP provisioning: SSO is the login path: it authenticates users and auto-provisions local profiles on first login. If your IdP also manages group membership or needs to create/deactivate accounts out-of-band, see SCIM Provisioning guide — the SCIM 2.0 server at /scim/v2/* covers the IdP provisioning path and complements SSO.

Quick start (Google OIDC)

In wikantik-custom.properties:

wikantik.baseURL = https://wiki.example.com/
wikantik.sso.enabled = true
wikantik.sso.type = oidc
wikantik.sso.oidc.discoveryUri = https://accounts.google.com/.well-known/openid-configuration
wikantik.sso.oidc.clientId = <client-id>.apps.googleusercontent.com
wikantik.sso.oidc.clientSecret = <client-secret>
wikantik.sso.oidc.scope = openid profile email
# Google sends no preferred_username, so key the login name on email.
wikantik.sso.claimMapping.loginName = email

Register the redirect URI wikantik.baseURL + /sso/callback (e.g. https://wiki.example.com/sso/callback) with the provider. It must exactly match — for a single configured client Wikantik deliberately uses a clean, parameter-free callback URL (no ?client_name= suffix), because strict IdPs such as Google reject any query string on the redirect_uri.

wikantik.baseURL must be set to the public HTTPS origin. The callback URL is derived from it. If it is unset, callbacks fall back to a relative URL and OIDC redirect_uri validation will fail.

Property reference

All properties live in wikantik-custom.properties (or ini/wikantik.properties). Defaults are applied by SSOConfig.

Core

PropertyDefaultNotes
wikantik.sso.enabledfalseMaster switch. When false, no SSO clients are built.
wikantik.sso.typeoidcoidc, saml, or both.
wikantik.sso.autoProvisiontrueCreate a local profile on first successful SSO login.
wikantik.sso.identityClaimsubIdP claim treated as the immutable account-link key. See Identity binding.
wikantik.baseURL—Public HTTPS origin; the /sso/callback URL is derived from it. Required.

OIDC

Required when type is oidc or both. If any required property is missing, the OIDC client is skipped and an error is logged (SSO does not fail closed — other configured clients still load).

PropertyDefaultNotes
wikantik.sso.oidc.discoveryUri—Provider OpenID configuration document URL. Required.
wikantik.sso.oidc.clientId—OAuth client ID from the provider console. Required.
wikantik.sso.oidc.clientSecret—OAuth client secret (sensitive — keep out of git). Required.
wikantik.sso.oidc.scopeopenid profile emailSpace-delimited scopes.

Client authentication is pinned to client_secret_basic to keep pac4j from auto-selecting private_key_jwt.

SAML 2.0

Required when type is saml or both. If identityProviderMetadataPath or serviceProviderEntityId is missing, the SAML client is skipped and an error is logged.

PropertyDefaultNotes
wikantik.sso.saml.identityProviderMetadataPath—Path/URL to the IdP metadata XML. Required.
wikantik.sso.saml.serviceProviderEntityId—SP entity ID. Required.
wikantik.sso.saml.keystorePath—JKS keystore for SP signing/encryption keys.
wikantik.sso.saml.keystorePassword—Keystore password (sensitive).
wikantik.sso.saml.privateKeyPassword—Private-key password (sensitive).
wikantik.sso.saml.serviceProviderMetadataPath—Optional writable path where pac4j persists generated SP metadata (incl. the registered ACS URL). Without it pac4j holds SP metadata only in memory, which can break ACS-URL resolution against some IdPs.
wikantik.sso.saml.authnRequestBindingTypepac4j defaultOnly HTTP-Redirect is supported (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect). Do not set HTTP-POST — the redirect servlet does not render a form-post AuthnRequest.

With type = saml the Assertion Consumer Service (ACS) URL is registered as exactly the /sso/callback URL. With type = both the OIDC and SAML clients share that endpoint, so the ACS URL carries ?client_name=SAML2Client and you register that exact URL with the IdP.

Claim mapping

IdP claims are mapped to the local profile fields. Defaults suit a standard OIDC IdP; Google needs loginName remapped to email (it sends no preferred_username).

PropertyDefaultMaps to
wikantik.sso.claimMapping.loginNamepreferred_usernameWiki login name
wikantik.sso.claimMapping.fullNamenameDisplay name (first sign-in only)
wikantik.sso.claimMapping.emailemailEmail

SSOLoginModule does not read wikantik.sso.* itself. It reads JAAS options (sso.claimLoginName, sso.claimFullName, sso.claimEmail, sso.identityClaim). When wikantik.sso.enabled = true, DefaultAuthenticationManager.initSSOConfig copies the wikantik.sso.claimMapping.* and wikantik.sso.identityClaim values into those options at startup, so you set them once under wikantik.sso.* and need no extra step. If you set an option explicitly as wikantik.loginModule.options.sso.claimLoginName (or .sso.claimFullName, .sso.claimEmail, .sso.identityClaim), your explicit value wins over the bridged one. A claim-mapping change takes effect after a restart. The claimMapping.* keys are read by prefix and are not listed individually in wikantik.properties; the defaults above are the built-in ones.

Claim values are sanitised before use: multi-valued claims are normalised to their first scalar element, and a login name from the IdP is rejected when it is blank, longer than 100 characters, or contains whitespace or control characters. If the mapped claim is absent, the login name falls back to the profile's username and then its id (for Google, the numeric sub).

Display name at first sign-in

People are shown to each other by display name, never by login (with claimMapping.loginName = email the login is an email address). When SSO creates an account (wikantik.sso.autoProvision), its display name comes from the fullName claim (default name) if the claim passes the display-name rules: 2 to 64 characters, no @, no control or invisible formatting characters, not reserved, not another account's login, and not already another account's display name (ignoring case). If the claim is taken, because another account already has that display name or login, a number is added: Jane Doe 2, Jane Doe 3. If the claim is missing or unusable (it is reserved, contains @, as an email address does, contains a control or invisible formatting character, or is too short or too long) the account gets the placeholder Member N, the lowest free N. The login or email is never used. SSO provisioning does not fail on a name conflict; it logs the choice at INFO. The name is not refreshed from the IdP on later sign-ins, so users change it themselves under Preferences.

Identity binding and account linking

SSO links an IdP identity to a local account on the claim named by wikantik.sso.identityClaim (default sub, the IdP's immutable subject). Set it to preferred_username only to deliberately trust a mutable claim.

Security behaviour

Container / Docker configuration

In Docker deployments, the entrypoint injects SSO properties from WIKANTIK_SSO_* environment variables (set in .env.prod, never in git). See .env.example and docker/entrypoint.sh.

Environment variableProperty
WIKANTIK_SSO_ENABLEDwikantik.sso.enabled
WIKANTIK_SSO_TYPEwikantik.sso.type
WIKANTIK_SSO_OIDC_DISCOVERY_URIwikantik.sso.oidc.discoveryUri
WIKANTIK_SSO_OIDC_CLIENT_IDwikantik.sso.oidc.clientId
WIKANTIK_SSO_OIDC_CLIENT_SECRETwikantik.sso.oidc.clientSecret
WIKANTIK_SSO_OIDC_SCOPEwikantik.sso.oidc.scope
WIKANTIK_SSO_IDENTITY_CLAIMwikantik.sso.identityClaim
WIKANTIK_SSO_AUTO_PROVISIONwikantik.sso.autoProvision
WIKANTIK_SSO_CLAIM_LOGIN_NAMEwikantik.sso.claimMapping.loginName
WIKANTIK_SSO_CLAIM_FULL_NAMEwikantik.sso.claimMapping.fullName
WIKANTIK_SSO_CLAIM_EMAILwikantik.sso.claimMapping.email

The container block is OIDC-oriented: it skips entirely unless WIKANTIK_SSO_ENABLED=true, and the SAML properties are not passed through the entrypoint. For SAML in a container, mount a wikantik-custom.properties fragment with the wikantik.sso.saml.* keys, or extend docker/entrypoint.sh.

The redirect URI registered with the provider must be WIKANTIK_BASE_URL + /sso/callback, so WIKANTIK_BASE_URL must be the public HTTPS origin (Google SSO is live in production behind cloudflared at wiki.wikantik.com via this mechanism).

Troubleshooting

SymptomLikely cause
redirect_uri_mismatch from the IdPRegistered redirect URI ≠ baseURL + /sso/callback, or wikantik.baseURL unset/incorrect.
Login succeeds but the wiki login name is the numeric subclaimMapping.loginName not mapped to email for a provider (e.g. Google) that omits preferred_username.
SSO login refused for a known userName collides with a non-SSO local account lacking the sso.subject marker — fail-closed by design. Link or rename the local account.
SAML assertion rejected on Destination mismatchACS URL / serviceProviderMetadataPath not aligned with the public callback URL; confirm HTTP-Redirect binding.
Mapped claims appear ignoredwikantik.sso.enabled is not true (the bridge runs only then), a wikantik.loginModule.options.sso.* override is set, or Tomcat was not restarted after the change.
Server starts fine, but every SSO login redirects to sso_redirect_failed — and the boot log has a line like OIDC discovery self-check FAILED for <uri> after 3 attempts: ... — the identity provider was UNREACHABLE from this host on every attempt (check outbound network egress / DNS / TLS) (or the "reachable but did not return its discovery document" / "not a valid OIDC discovery document" variants)OidcDiscoverySelfCheck probes the OIDC discovery URL at startup, on a background daemon thread (up to 3 attempts, 2 seconds apart, 10-second timeout each) — it never blocks or fails boot, it only logs, and only when every attempt has failed. pac4j itself fetches discovery lazily on the first /sso/login request and caches it, so without this log line a lost egress path (firewall, DNS, proxy) looks like the app started healthy and only breaks when a user actually tries to log in. Fix egress/DNS/TLS to the discovery URL (or correct wikantik.sso.oidc.discoveryUri) and restart — a lazy pac4j fetch means a config fix alone, without a restart, may not clear a bad cached state.

Cluster Hub: Wikantik Platform Development Hub: Architecture, MCP Protocols, and Agent Briefings (wikantik-development)