Keyway SSO for Craft CMS › Guides

Craft CMS SSO group mapping: turning directory groups into Craft user groups

Mapping groups is the easy half. The half worth reading twice is what a login is allowed to write — because the same table that grants an editor their permissions can take an administrator's away.

How a group arrives, and how it becomes a Craft group

Group membership travels as an ordinary attribute or claim. Source attributes lists the ones that carry it, one per line; the default is groups, which is what a Keycloak group mapper or an Okta group attribute statement produces. Each row of the Groups table then maps one identity-provider group to a Craft group handle.

SettingWhat it does
MatchExact, Prefix or Suffix. There are no regular expressions, deliberately: an administrator-supplied pattern is an attack surface, and a rule like .* silently matching every directory group into an admin rule is the failure that matters. Prefix and suffix patterns must be at least two characters long.
Craft group handleMust start with a letter and contain only letters, digits and underscores, 64 characters at most.
Case-sensitive matchingOff by default, so matching ignores case until you turn it on.
Default groupA handle everybody signing in joins. Leave it empty for none.
Sync modeAppend (default) keeps the groups the account already has and adds the mapped ones. Replace makes the provider the only source of truth, so a login that maps to nothing strips the account of every group it has.
Deny login when no group matchesRefuses the sign-in with no_group_match when none of the provider's groups matched a rule. Note that the default group deliberately does not satisfy this check — it is a floor, not a match.

If a group value looks right and still does not match, read it from the diagnostics row rather than from the directory: Details → Mapped to shows matchedIdpGroups, unmatchedIdpGroups and craftGroups in full, unmasked, because they are the thing being debugged. The same values under Attributes received are masked like any other claim (en***ng), so comparing against that block will mislead you.

What a login writes, and when it writes nothing

This is the condition to hold on to, because every dangerous setting below depends on it:

Attributes, the admin flag and group membership are written only for a decision of jit_create (a new account) or update_on_login (an existing account with Update accounts on every login switched on). With that switch off the decision is existing_unchanged and nothing is applied — not Replace, not admin revocation, not a single mapped field.

The trap is the reverse of the obvious one: testing Replace with sync-on-login switched off shows a login that changes nothing, which is not evidence that the setting is safe. Check the diagnostics row first — if it says existing_unchanged, the mapping was never applied. Test Replace and admin revocation on a throwaway account.

Two mistakes cost real access. A Replace sync that removes the group granting control panel access leaves that account signed in with nowhere to go, or refused with no_cp_access — which is also what a just-created account gets when its mapped groups grant no control panel permission. And Revoke admin when no rule matches clears the Craft admin flag of anybody who signs in without matching an admin rule, including the account that configured the plugin.

Making somebody an administrator takes two switches

Group rules cannot grant admin. Raising an account to Craft administrator needs a separate rule type — an admin rule, not a group rule — and Allow admin escalation turned on, which it is not by default. The Admin rules table and Revoke admin when no rule matches only appear once you turn it on, and the settings screen keeps a standing warning while it is on: "Your identity provider can grant Craft admin status through the admin rules below."

The reason is one sentence long: with escalation on, anybody who can edit groups in your directory can make themselves a Craft admin. Revoking admin is a separate switch, and it is off by default for the mirror-image reason — an identity provider outage must not be able to demote the owner of the site.

Attribute mapping writes to a whitelist, and nothing else

The same table that maps groups also maps attributes onto user fields, and the set of writable targets is fixed: email, username, firstName, lastName, fullName, and field:<handle> for a custom field on the user's field layout. Anything else is refused — the mapping layer raises an error instead of writing. Names such as enabled, invalidLoginCount, lockoutDate, archived and currentPassword are kept in the code purely so that aiming at one of them produces a readable message; earlier builds used a blacklist, which is a gate that stands open by default.

Two extra sources exist: @nameId and @issuer read the assertion itself rather than an attribute. Multiple values decides what happens when an attribute arrives more than once (First, Last, Join), and Transform can force a value to lower or upper case.

Leaving the Attributes table empty applies the shipped defaults — email (required), firstName, lastName — and a missing required attribute refuses the login with attributes_rejected. A username row is optional: when a new account is created and nothing maps to a username, the e-mail address becomes the Craft username.

Just-in-time accounts, and the two linking switches

Create accounts on first login and Update accounts on every login are on by default, and Match existing accounts by is E-mail. With creation off, an unmatched identity is refused with jit_disabled rather than let in.

A Craft account that already exists with the same address is a different question, and the answer is two separate opt-ins:

When you enable linking, fill in Allowed e-mail domains: one per line, example.com matches exactly, .example.com matches sub-domains only, empty means any domain. An address outside the list is refused with domain_not_allowed. On a site with public registration this matters more than it looks — an account registered with somebody else's address and never verified can be linked this way.

Account state is checked before any mapping applies

An account has a state of its own, and single sign-on does not overrule it. Accounts are modelled as active, pending, suspended or inactive, plus a separate "locked" flag from repeated failed sign-ins.

Each of those is fixed in the Craft control panel, not on the plugin's settings screen. If a login succeeds but part of the mapping could not be applied — a Craft group handle that does not exist on this site, or a custom field that is not in the user's field layout — the row carries the notice provisioning_incomplete and names the handle. That person is signed in with a permission set that is not the one on your settings screen, and nothing else would tell you.

One more thing that looks like a mapping bug and is not: the Groups stage exists as a label in the diagnostics screen, but no code path writes a row with it. Group results are reported inside the Provisioning row, under Details → Mapped to. The rest of the codes are on the troubleshooting page.

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