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.
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.baseURLmust 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 OIDCredirect_urivalidation will fail.
All properties live in wikantik-custom.properties (or
ini/wikantik.properties). Defaults are applied by SSOConfig.
| Property | Default | Notes |
|---|---|---|
wikantik.sso.enabled | false | Master switch. When false, no SSO clients are built. |
wikantik.sso.type | oidc | oidc, saml, or both. |
wikantik.sso.autoProvision | true | Create a local profile on first successful SSO login. |
wikantik.sso.identityClaim | sub | IdP 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. |
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).
| Property | Default | Notes |
|---|---|---|
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.scope | openid profile email | Space-delimited scopes. |
Client authentication is pinned to client_secret_basic to keep pac4j from
auto-selecting private_key_jwt.
Required when type is saml or both. If identityProviderMetadataPath or
serviceProviderEntityId is missing, the SAML client is skipped and an error
is logged.
| Property | Default | Notes |
|---|---|---|
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.authnRequestBindingType | pac4j default | Only 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.
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).
| Property | Default | Maps to |
|---|---|---|
wikantik.sso.claimMapping.loginName | preferred_username | Wiki login name |
wikantik.sso.claimMapping.fullName | name | Display name (first sign-in only) |
wikantik.sso.claimMapping.email | email |
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).
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.
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.
sso.subject marker attribute.sso.subject
marker fails closed (login is refused) rather than silently taking over
the account./sso/callback is exempt from the CSRF
synchronizer-token filter — the IdP-signed assertion is its own CSRF defense.
OIDC uses the standard authorization-code flow./login SPA route with an
?error=<code> parameter that LoginPage surfaces. /login is dual-registered
(in web.xml and SpaRoutingFilter.SPA_EXACT) so it renders rather than 404s.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 variable | Property |
|---|---|
WIKANTIK_SSO_ENABLED | wikantik.sso.enabled |
WIKANTIK_SSO_TYPE | wikantik.sso.type |
WIKANTIK_SSO_OIDC_DISCOVERY_URI | wikantik.sso.oidc.discoveryUri |
WIKANTIK_SSO_OIDC_CLIENT_ID | wikantik.sso.oidc.clientId |
WIKANTIK_SSO_OIDC_CLIENT_SECRET | wikantik.sso.oidc.clientSecret |
WIKANTIK_SSO_OIDC_SCOPE | wikantik.sso.oidc.scope |
WIKANTIK_SSO_IDENTITY_CLAIM | wikantik.sso.identityClaim |
WIKANTIK_SSO_AUTO_PROVISION | wikantik.sso.autoProvision |
WIKANTIK_SSO_CLAIM_LOGIN_NAME | wikantik.sso.claimMapping.loginName |
WIKANTIK_SSO_CLAIM_FULL_NAME | wikantik.sso.claimMapping.fullName |
WIKANTIK_SSO_CLAIM_EMAIL | wikantik.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).
| Symptom | Likely cause |
|---|---|
redirect_uri_mismatch from the IdP | Registered redirect URI ≠ baseURL + /sso/callback, or wikantik.baseURL unset/incorrect. |
Login succeeds but the wiki login name is the numeric sub | claimMapping.loginName not mapped to email for a provider (e.g. Google) that omits preferred_username. |
| SSO login refused for a known user | Name 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 mismatch | ACS URL / serviceProviderMetadataPath not aligned with the public callback URL; confirm HTTP-Redirect binding. |
| Mapped claims appear ignored | wikantik.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)