Keyway SSO for Craft CMS › Guides
Craft CMS has no built-in SAML sign-in for the control panel, so it takes a plugin. This page is the provider-independent half of the job: what the three values are, in which order to fill them in so the save actually goes through, and what the plugin checks before it lets anybody in.
| Value | What it is | Where it comes from |
|---|---|---|
| ACS URL | The assertion consumer service: the address your provider POSTs the signed assertion to. | This site. Copy it from ACS URL this site answers on on the settings screen — it is <BASE>/actions/keyway-sso/sso/acs. |
| SP entity ID | An identifier for this site, not an address. Any stable string works as long as both sides carry exactly the same one. | You choose it. The usual choice is this site's base URL without a trailing slash, offered as a copy field above the SP entity ID box. |
| Metadata | An XML document describing this service provider, which some consoles can import instead of retyping. | This site, at <BASE>/actions/keyway-sso/sso/metadata and behind the Download metadata button — but only after a complete save, see below. |
From the provider you need three things in return: its IdP entity ID (the issuer it stamps on assertions), its IdP SSO URL (where the browser is sent to log in) and its IdP signing certificate. The certificate has to be the full X.509 certificate, PEM or bare base64 — a fingerprint is rejected, because a fingerprint proves which certificate the message carried, not which one you trust.
The settings screen is validated as a whole. With Protocol set to
SAML 2.0, 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. So:
SAML 2.0, and copy the ACS URL and the site URL. Both are readable before you save
anything, because the plugin builds them from its own registered routes.Before the first complete save, the metadata address answers 404 and the download button is inert. That is deliberate, not a fault: a metadata document naming a blank entity ID would be accepted by the provider and would configure it against a sign-in that can never validate. The metadata file is a way to hand over a finished configuration, not a way to start one.
Note what the document does not carry, so nothing surprises you: no
KeyDescriptor (the plugin has no field for an SP certificate), a
NameIDFormat of unspecified, AuthnRequestsSigned="false",
and no SingleLogoutService until logout is configured.
ds:Signature child, a Reference
pointing elsewhere or a missing enveloped-signature transform is refused as
signature_coverage. If you see it, something is re-wrapping the response —
investigate that rather than relaxing anything, because nothing here can be relaxed.DOCTYPE declaration is refused outright, as an
entity-expansion defence.The Name ID has to be stable. Accounts the plugin creates are recognised on
later logins by account, issuer and subject — the NameID. Use
persistent or emailAddress. transient is a fresh value on
every login by definition, so every second login of every user is refused with
linking_disabled. The plugin never reads the format attribute, so this is a
configuration rule, not a check you can fail, and there is no error message pointing at it.
Logins must start on the Craft side. The plugin requires its own
RelayState and a matching InResponseTo, so an assertion belonging to no
login of ours is refused as unsolicited_response. In practice: the dashboard tile for
this application will not sign anybody in. Send people to the Craft login screen and let them
press the SSO button.
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.
Mechanically, on a SAML installation that opts in: the endpoint is
/actions/keyway-sso/sso/slo, HTTP-Redirect binding
only, and it stays shut until IdP single logout URL and an SP private
key are both set — until then it refuses every message and the metadata document
deliberately advertises no SingleLogoutService, because advertising one would make
every provider that reads the document send logout requests to an address that answers nothing. A
request is honoured only when its subject matches the session in the browser that carried it; a
mismatch ends nothing and is answered UnknownPrincipal, which is also what a browser
with no session gets.
Open Settings → Plugins → Keyway SSO and read the warnings at the top, then press Open sign-in diagnostics. Every attempt is a row with its stage, outcome, a machine reason code and a Details panel holding the attributes that arrived, what they mapped to and the decision taken. The person signing in only ever sees one vague sentence — that is deliberate, so that the login endpoint cannot be used to tell an existing account from a missing one.
| Reason code | What it means |
|---|---|
confirmation_invalid, destination_mismatch | The assertion's Recipient or the response's Destination is not literally equal to the configured ACS URL. Put the three addresses side by side: the one registered at the provider, the one in the ACS URL field, and the read-only ACS URL this site answers on. A trailing slash is enough. |
audience_mismatch | The AudienceRestriction does not name this SP entity ID. |
issuer_mismatch | The assertion issuer is not the configured IdP entity ID, compared byte for byte. |
signature_invalid | The wrong certificate in IdP signing certificate, or the provider rotated its key. |
assertion_expired | A time window did not check out. Compare the clocks on both machines before touching anything else. |
attributes_rejected | A required attribute was not sent, or a value mapped to email is not a valid address. Details → Attributes received is the ground truth about what your provider sent. |
no_group_match, jit_disabled, linking_disabled | The login was understood and then refused by your own policy. The fix is on the Craft settings screen, not at the provider. |
binding_not_issued | A notice, not a refusal: the login ran without the cookie tying it to the browser that started it, because the callback is not served over HTTPS. |
The full table, including the codes that appear only on unusual installations, is on when single sign-on does not sign anybody in. Provider specifics: Okta, Keycloak.
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.