Keyway SSO for Craft CMS › Guides
Both protocols, in the order that actually works: read the addresses in Craft, build the client in Keycloak, then come back and save a complete configuration.
Keycloak is the provider this plugin has been exercised against most directly, and the two protocols are not at the same level of proof. Saying so up front is cheaper than finding out later.
| Protocol | Status |
|---|---|
| SAML 2.0 | Verified end to end on 17 September 2026 against a live Keycloak 26.0 realm and a live Craft install: sign-in, attribute and group mapping, account creation, and the opt-in, SAML-only, one-way logout path of step 7 (identity provider → Craft, which will not work with Okta). |
| OpenID Connect | Has not been run end to end. A smoke check (bin/smoke-keycloak.php) reads a real realm's discovery document and JWKS and verifies a real id token's key selection and signature — but not the HTTPS transport (the scheme is rewritten before the document is handed over) and not the full id-token reader. |
dom, mbstring, openssl and zlib extensions. zlib is declared as a requirement, so Composer refuses to install without it: the SAML authentication request is DEFLATE-encoded before it is sent.manage-clients in that realm, or realm admin), plus the realm's base URL and realm name — everything Keycloak exposes hangs off <KEYCLOAK_BASE>/realms/<REALM>.https, with a redirect URI that carries no query string.Not published yet. Keyway SSO is in final testing and is not yet in the Craft Plugin Store or on Packagist, so the install commands below do not resolve today. They are here so you can see what installation will look like; this page will be updated when the plugin is listed.
Install from the project root of the Craft site with composer require aphexcoding/keyway-sso and php craft plugin/install keyway-sso. After any later update, run php craft up — a site that already had the plugin gets new tables only that way.
SAML 2.0 or OpenID Connect. Until you do, Protocol sits at Disabled (password login only) and the login screen looks exactly as it did before.index.php?p=, this site generates URLs with the script name in them. For SAML that is acceptable. For OpenID Connect, turn on omitScriptNameInUrls in the Craft general config first — several providers refuse a redirect URI with a query string.Do not try to save a half-filled SAML configuration, and do not expect the metadata file yet. The settings screen is validated as a whole: with SAML 2.0 selected, a save is rejected until IdP entity ID, IdP signing certificate, IdP SSO URL, SP entity ID and ACS URL are all present and well-formed, and a rejected save stores nothing. The metadata URL answers 404 until that first complete save.
email and have Keycloak send the user's e-mail address as the subject. The value has to be stable: accounts this plugin creates are recognised on later logins by (account, issuer, Name ID), so transient makes every second login fail with linking_disabled. Use email or persistent.email, firstName and lastName, mapped to the Keycloak user properties of the same names. Avoid the predefined X.500 mappers unless you also change the mapping table — they send names like urn:oid:1.2.840.113549.1.9.1.groups, with the full-group-path option off.Then collect what Craft needs from the realm's SAML descriptor, at <KEYCLOAK_BASE>/realms/<REALM>/protocol/saml/descriptor:
| Craft field | Value in the descriptor |
|---|---|
| IdP entity ID | the entityID attribute — <KEYCLOAK_BASE>/realms/<REALM> |
| IdP SSO URL | the SingleSignOnService location for the HTTP-Redirect binding — <KEYCLOAK_BASE>/realms/<REALM>/protocol/saml |
| IdP signing certificate | the X509Certificate element inside KeyDescriptor use="signing", as PEM or bare base64 — a fingerprint is rejected |
S256 — but then leave Client secret empty in Craft. An empty-looking secret that is really an unresolved environment reference is treated as "no secret", so the plugin refuses to sign anyone in until the reference resolves.profile and email scopes, and read the client secret from its credentials if you made it confidential.groups, included in the ID token, full-path option off. The plugin reads identity from the id token; userinfo is consulted only when Fetch userinfo is on, and even then the id token stays the source of identity.<KEYCLOAK_BASE>/realms/<REALM> — the same string Keycloak puts in the iss claim, compared byte for byte, trailing slash included. The plugin appends /.well-known/openid-configuration itself; do not paste the discovery URL.SAML takes IdP entity ID, IdP signing certificate, IdP SSO URL, SP entity ID (the SAML client ID) and ACS URL. SP private key is needed only for encrypted assertions and for step 7. Clock skew defaults to 60 seconds and is capped at 120.
OpenID Connect takes Issuer and Redirect URI (https only, no exception and no override), Client ID, Client secret, and Scopes one per line — the default is openid, profile, email, and openid is added automatically.
Secrets belong in environment variables. SP private key and Client secret are the only two fields that read one; write them as $KEYWAY_SP_KEY and $KEYWAY_OIDC_SECRET. Every other field is stored as typed, so $SOMETHING in, say, SP entity ID is taken literally and the login fails on the audience check.
After the save, the screen compares what you typed into ACS URL (or Redirect URI) with the address this site really answers on and comments underneath the field: silence means the two are identical. If something is still wrong, the top of the screen says "Single sign-on is selected but the connection is not usable yet, so the login screen keeps password login only."
An empty Attributes table does not mean "map nothing" — it means "use the shipped defaults": email → email (required), firstName → firstName, lastName → lastName. That fits the SAML mappers above exactly. It does not fit OpenID Connect, because Keycloak's standard claims are given_name and family_name — add those two rows plus email yourself.
Group rules match an identity-provider group to a Craft group handle by Exact, Prefix or Suffix. With Keycloak's full-group-path option on, values arrive as /parent/child with a leading slash — either leave the option off, or write the rules to match the path form. The whole mapping model, including admin escalation and account linking, is on the group mapping page.
Before telling anyone that single sign-on is live, keep at least one Craft admin whose password you know, and confirm that password still works. Admins may still use a password is on by default and is what keeps that answer safe; emergency accounts and the 24-hour break-glass window are on the password login page.
Then open the control-panel login screen in a private window. The SSO button appears only when the configuration is actually usable, so a missing button sends you back to the warnings on the settings screen. Whatever happens, open Open sign-in diagnostics at the bottom of the settings screen: every attempt is one row with a stage, an outcome and a machine reason code, and Details shows the attributes that arrived and the decision. A login has five minutes between pressing the button and coming back; after that the single-use login state has expired. Reason codes are listed on the troubleshooting page.
Single Logout (SAML only, one direction, opt-in: it needs your provider's logout URL and an SP private key before it does anything): when your identity provider sends a signed SAML LogoutRequest, the plugin ends the matching Craft session. Signing out of Craft does not end the session at your identity provider, and Okta will not send such a request after an ordinary Okta sign-out — so with Okta as your only provider, plan on single sign-on, not single logout. Verified end to end against Keycloak 26.0; OpenID Connect has no logout support.
On the Keycloak side, on the SAML client from step 2: turn Front channel logout on and put the plugin's SP single logout URL copy field (<BASE>/actions/keyway-sso/sso/slo) into Logout service redirect binding URL. The plugin reads logout messages on the HTTP-Redirect binding only, so leave the POST-binding field empty. On the Craft side, fill in IdP single logout URL (<KEYCLOAK_BASE>/realms/<REALM>/protocol/saml, the same address as the SSO URL) and SP private key, which signs the LogoutResponse. Until both are set, sso/slo refuses every message and the metadata advertises no SingleLogoutService — deliberately, because an advertised endpoint that answers nothing makes every provider reading the document send logout requests to a dead address.
What the live test showed: a sign-out in Keycloak reaches sso/slo, the plugin answers with a signed LogoutResponse carrying Success, and the control panel bounces to the login screen on the next request. A browser with no Craft session is answered Requester / UnknownPrincipal and Keycloak ends its own session regardless; that is the normal answer to "there was nothing here to end", not a fault to chase. Not checked: the same user signed in from several browsers at once, and this logout path against any provider other than Keycloak.
WantAuthnRequestsSigned="true" and a new SAML client requires a client signature by default, so turning that requirement off for this client is mandatory, not optional.https on both sides — issuer and redirect URI — with no loopback exception and no override. A Keycloak speaking plain HTTP, the usual local-development setup, cannot be used over OIDC; put a certificate on it, or use the SAML path while developing.RS256 and ES256 id-token signatures are accepted. Keycloak's default realm key is RS256, so this matters only if somebody changed it.SAML 2.0 and OpenID Connect sign-in for the Craft control panel: attribute and group mapping, just-in-time accounts, an admin password fallback with a guard that switches admin password login back on, with a warning, if your settings would otherwise leave nobody able to sign in, and a diagnostics screen that tells you why a login was refused. The plugin is in final testing and is on its way to the Craft Plugin Store.