Keyway SSO for Craft CMS › Guides

Craft CMS Keycloak SSO: signing in to the control panel from a Keycloak realm

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.

What has actually been verified

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.

ProtocolStatus
SAML 2.0Verified 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 ConnectHas 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.

What you will need

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.

Step 1 — turn the plugin on and copy this site's values

  1. Open Settings → Plugins → Keyway SSO and set Protocol to 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.
  2. Copy the addresses this installation answers on — do not retype them. For SAML: ACS URL this site answers on and This site's URL. For OIDC: Redirect URI this site answers on. They are on the screen before you save anything.
  3. If the copied address contains 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.

Step 2 — create the client in Keycloak

SAML

  1. In the realm, go to Clients and create a client of type SAML. A SAML client's Client ID in Keycloak is the SP entity ID, so it must be exactly the string you will put in SP entity ID on the Craft side — the This site's URL copy field is the usual choice.
  2. Set the client's valid redirect URI and its SAML processing URL to the ACS URL this site answers on value. Keycloak posts the assertion to that address.
  3. In the client's key settings, turn off the requirement for a signed client (AuthnRequest) signature. This plugin sends authentication requests unsigned, deliberately, and Keycloak enables that requirement by default — leaving it on makes every login fail at Keycloak before Craft sees a response.
  4. Leave assertion signing on: a signed assertion is required unconditionally and there is no setting to relax it.
  5. Set Name ID format to 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.
  6. In the client's dedicated client scope, add three "user property" mappers so the assertion carries attributes named exactly 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.
  7. For group mapping, add a group-list mapper whose attribute name is 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 fieldValue in the descriptor
IdP entity IDthe entityID attribute — <KEYCLOAK_BASE>/realms/<REALM>
IdP SSO URLthe SingleSignOnService location for the HTTP-Redirect binding — <KEYCLOAK_BASE>/realms/<REALM>/protocol/saml
IdP signing certificatethe X509Certificate element inside KeyDescriptor use="signing", as PEM or bare base64 — a fingerprint is rejected

OpenID Connect

  1. Create a client of type OpenID Connect; its Client ID is what goes into Client ID on the Craft side.
  2. Turn client authentication on for a confidential client. A public client also works — the plugin always uses PKCE with 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.
  3. Set the valid redirect URI to the Redirect URI this site answers on value, exactly as shown. Keycloak accepts wildcards here; do not use one — the plugin compares the registered value with what it sends.
  4. Make sure the client can issue the profile and email scopes, and read the client secret from its credentials if you made it confidential.
  5. For group mapping, add a group-membership mapper to the dedicated scope with the token claim name 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.
  6. The value Craft needs as Issuer is <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.

Step 3 — paste Keycloak's values back into the plugin

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."

Step 4 — attributes and groups

An empty Attributes table does not mean "map nothing" — it means "use the shipped defaults": emailemail (required), firstNamefirstName, lastNamelastName. 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.

Steps 5 and 6 — keep a way back in, then test the login

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.

Step 7 — Single Logout: SAML only, one way, opt-in, and not with Okta

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.

Known limits with Keycloak

Keyway SSO for Craft CMS 5

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.

Read the deployment documentation