dmlab / module-admin-sso
Admin panel SSO capability for Magento 2 — provider-agnostic OIDC or SAML 2.0 login for the admin panel (JIT admin-user creation, IdP-group to ACL-role mapping, enforce-SSO + break-glass).
Package info
github.com/dmlab-work/module-admin-sso
Type:magento2-module
pkg:composer/dmlab/module-admin-sso
Requires
- php: ~8.2.0||~8.3.0||~8.4.0||~8.5.0
- ext-openssl: *
- dmlab/module-sso-core: ^0.3.0
- magento/framework: ^103.0.7
- magento/module-authorization: ^100.4.7
- magento/module-backend: ^102.0.7
- magento/module-config: ^101.2.7
- magento/module-user: ^101.2.7
Requires (Dev)
- magento/module-security: ^100.4.7
- magento/module-two-factor-auth: ^1.1.6
- phpunit/phpunit: ^11.5||^12.5
Suggests
- dmlab/module-admin-scim: Automatic admin offboarding
- magento/module-security: Tracks SSO admin sessions and applies the Admin Account Sharing setting
- magento/module-two-factor-auth: Grants the 2FA session to SSO logins so the IdP MFA is not repeated
Provides
None
Conflicts
None
Replaces
None
README
Provider-agnostic single sign-on for the Magento 2 admin panel (OIDC or SAML 2.0).
Every admin user in Magento has its own password, staff who leave keep their access until someone remembers to remove them, and paid SSO extensions cap how many users you may have.
Admin SSO lets staff sign into the Magento admin with their company account — Okta, Microsoft Entra ID or Google Workspace via a provider plugin, any OpenID Connect provider (Keycloak, Authentik, Auth0, …) through the built-in Generic OIDC, or any SAML 2.0 IdP (AD FS, Entra ID, Okta, Keycloak) through the built-in Generic SAML 2.0. Admin accounts are created on first login and roles come from IdP groups (or a default role); password login can be switched off, with a break-glass list for emergencies. Free, no user cap.
Install
Install a provider plugin (dmlab/module-admin-sso-okta, -azure or -google); it pulls this module and its dependencies:
composer require dmlab/module-admin-sso-okta bin/magento module:enable DmLab_SsoCore DmLab_AdminSso DmLab_AdminSsoOkta bin/magento setup:upgrade
For Entra ID or Google, require dmlab/module-admin-sso-azure / -google and enable DmLab_AdminSsoAzure / DmLab_AdminSsoGoogle instead.
For any other OIDC provider (Generic OIDC) or any SAML 2.0 IdP (Generic SAML 2.0), install this module alone — no plugin needed:
composer require dmlab/module-admin-sso bin/magento module:enable DmLab_SsoCore DmLab_AdminSso bin/magento setup:upgrade
OIDC: register the callback URL in your IdP: https://<admin-host>/<admin-path>/adminsso/sso/callback/ (with the trailing slash), where <admin-path> is the backend frontName (default admin). It must exactly match the admin URL used at runtime. SAML: see SAML.
Requirements
- Magento 2.4.7+ · PHP 8.2–8.5
- A provider plugin (Okta, Microsoft Entra ID, Google Workspace), any OIDC provider via Generic OIDC, or any SAML 2.0 IdP via Generic SAML 2.0
ext-openssl- The admin callback URL registered at the IdP (see Install)
- SAML: the admin served over HTTPS (the request-state cookie is
Secure; SameSite=None) Magento_TwoFactorAuthoptional — MFA done at the IdP is honoured (see 2FA)
Pairs with admin-scim for automatic admin offboarding.
Configuration
Admin → Stores → Configuration → DMLab → Admin SSO → General (config path dmlab_admin_sso/general/*).
| Field | Path | Notes |
|---|---|---|
| Enable Admin SSO | enabled |
Master switch; off by default. |
| Identity Provider | active_provider |
Generic OIDC, Generic SAML 2.0 and the presets of installed provider plugins. |
| Client ID | client_id |
OIDC client id from the IdP (not used by SAML). |
| Client Secret | client_secret |
Stored encrypted; sensitive, so app:config:dump skips it (set per environment with config:sensitive:set). |
| Enforce SSO | enforce_sso |
Blocks native password login. See below. |
| Allow Break-Glass Login | break_glass |
Lets allowlisted users keep password login where SSO blocks it. Off by default. |
| Break-Glass Usernames | break_glass_usernames |
Admin usernames, one per line or comma-separated; case-insensitive. |
| Group to Role Map | group_role_map |
IdP group → ACL role rules. See below. |
| Default Role | default_role |
Fallback role when no group matches; empty = refuse login. |
| Trust IdP MFA | trust_idp_mfa |
On by default; skips Magento 2FA only with MFA evidence. See below. |
| Trusted ACR Values | trusted_acr_values |
Optional acr values that count as MFA evidence. |
| Sign Out of IdP on Logout | idp_logout |
Off by default. See below. |
A "Sign in with SSO" button appears on the admin login page when the module is enabled and a provider is selected, using the active preset's branding (the generic OIDC and SAML presets have no icon).
Group → role mapping
group_role_map takes one rule per line as idp_group=role_id, where role_id is a Magento admin role id (authorization_role with role_type G). Blank lines and # comments are ignored. A Magento admin holds one role, so the first line matching any of the user's groups wins (rule order, not IdP group order); a repeated group keeps its first line.
# IdP group Magento role id
admins=1
support-team=4
Roles are (re)assigned on every login, so IdP group changes take effect at next sign-in. An identity whose groups match no rule gets default_role. If that is empty (or the role id does not exist) the SSO login is refused before any admin is created or linked; the stored role of an existing admin is never demoted — deprovisioning is SCIM's job. If the IdP sends the groups as a distributed claim (_claim_names, e.g. Entra groups overage), the groups are incomplete and the login is refused too.
Any OIDC provider
Select Identity Provider = Generic OIDC, set Client ID / Secret under General, then fill DMLab → Admin SSO → Generic OIDC (config path dmlab_admin_sso/generic/*):
| Field | Path | Notes |
|---|---|---|
| Issuer URL | issuer_url |
Discovery is read from <issuer>/.well-known/openid-configuration; a full discovery URL also works. |
| Display Name | display_name |
Button label "Sign in with …"; default SSO. |
| Scopes | scopes |
Space-separated; default openid profile email; openid is always sent. |
| Groups Claim | groups_claim |
ID token claim with the user's groups; default groups; empty = Default Role only. |
| Extra Authorization Parameters | extra_auth_params |
One key=value per line (e.g. prompt=select_account); blank lines and # comments ignored. |
Validation on save: absolute https issuer (http only for localhost); valid scope tokens; extra parameters as unique key=value lines, protocol-reserved names (e.g. state, redirect_uri) refused.
Redirect URI: https://<admin-host>/<admin-path>/adminsso/sso/callback/. Post-logout redirect URI (only with Sign Out of IdP on Logout): https://<admin-host>/<admin-path>/admin/auth/login/. email_verified is needed only to adopt an existing admin by email; new admins are created without it.
Keycloak
- Manage realms → Create realm → Realm name
magento→ Create. - Clients → Create client → Client type OpenID Connect, Client ID
magento-admin→ Next → Client authentication On, Standard flow on → Next → Valid redirect URIs = the redirect URI, Valid post logout redirect URIs = the post-logout URI → Save. Copy the secret from the Credentials tab. - Clients →
magento-admin→ Client scopes →magento-admin-dedicated→ Configure a new mapper (or Add mapper → By configuration) → Group Membership: Namegroups, Token Claim Namegroups, Full group path Off, Add to ID token On → Save. With Full group path On, groups arrive as/magento-adminsand the map lines must use that form. - Groups → Create group
magento-admins. Users → Create user (Email; Email verified On to adopt an existing admin) → Credentials → Set password, Temporary Off → Groups → Join Group →magento-admins→ Join. - Magento: Identity Provider = Generic OIDC; Client ID / Secret; Issuer URL
https://<keycloak-host>/realms/magento; Display NameKeycloak; Groups Claimgroups; Group to Role Mapmagento-admins=1(role id).
Authentik
- Applications → Applications → New Application → with New Provider…: Name
Magento Admin, Slugmagento-admin→ provider type OAuth2/OpenID Connect → Client type Confidential; Redirect URIs: Strict, type Authorization, the redirect URI (add the post-logout URI as a Strict entry of type Logout if IdP logout is used); Signing Key: pick any — required, sso-core accepts only asymmetric signatures and without a key authentik signs with HS256. Note the Client ID and Client Secret. Bindings are optional (none = every user may sign in). - The default
profilescope mapping already sendsgroups(group names) in the ID token. Directory → Groups → createmagento-adminsand add users. - The default
emailmapping sendsemail_verified: false. Only to adopt existing admins by email: Customization → Property Mappings → New Property Mapping → Scope Mapping, Scope nameemail, expression per authentik's "Verify email address" docs; on the provider, Advanced protocol settings → Scopes, replace the defaultemailmapping with it (never both). - Magento: Issuer URL
https://<authentik-host>/application/o/magento-admin/; Groups Claimgroups; Group to Role Mapmagento-admins=1.
SAML (ADFS, Entra, Okta, Keycloak)
Select Identity Provider = Generic SAML 2.0, then fill DMLab → Admin SSO → SAML 2.0 (config path dmlab_admin_sso/saml/*). Group to Role Map, Default Role, Enforce SSO and break-glass under General apply as for OIDC.
SP Endpoints at the top of the group shows the three values to enter at the IdP:
- ACS URL (Reply URL):
https://<admin-host>/<admin-path>/adminsso/saml/acs/(HTTP-POST) - SP Entity ID (Audience): SP Entity ID below, or the SP metadata URL when empty
- SP Metadata URL:
https://<admin-host>/<admin-path>/adminsso/saml/metadata/
| Field | Path | Notes |
|---|---|---|
| Display Name | display_name |
Button label "Sign in with …"; default SSO. |
| IdP Metadata Source | idp_metadata_source |
url (default) or xml. |
| IdP Metadata URL | idp_metadata_url |
Absolute https URL; keep any query string (Entra ?appid=). |
| IdP Metadata XML | idp_metadata_xml |
The IdP EntityDescriptor, pasted as is; validated on save. |
| IdP Entity ID | idp_entity_id |
Only when the metadata describes several IdPs. |
| SP Entity ID | sp_entity_id |
Must equal the Entity ID / Audience at the IdP; empty = SP metadata URL. |
| NameID Format | name_id_format |
Default unspecified; persistent recommended. |
| SP Certificate | sp_certificate |
PEM; needed for signed AuthnRequests or encrypted assertions; published in the SP metadata. |
| SP Private Key | sp_private_key |
Unencrypted PEM matching the certificate; stored encrypted, sensitive. |
| Sign AuthnRequests | sign_authn_requests |
Off by default; needs certificate + key. |
| Require Encrypted Assertions | require_encrypted_assertions |
Off by default; needs certificate + key. Encrypted assertions are decrypted whenever a key is set. |
| Email / Name / Groups Attribute | email_attribute, name_attribute, groups_attribute |
Empty = defaults: email, mail; displayName, name (else givenName/firstName + surname/sn/lastName); groups, memberOf; plus the WS-Fed claim URIs and X.500 OIDs. |
| Subject Attribute | subject_attribute |
Single-valued attribute identifying the user; empty = NameID. |
| Trust IdP Email | trust_email |
Off by default. On = the asserted email counts as verified, so an existing admin with that email is adopted on first login. Enable only if the IdP controls the email. |
| Allow IdP-Initiated Login | allow_idp_initiated |
Off by default. See below. |
Assertions must be signed (RSA-SHA256+); a Response-only signature is rejected. The admin is linked by issuer = IdP entity ID and subject = NameID (or the subject attribute). Run bin/magento setup:upgrade once for sso-core's assertion replay table.
Keycloak
- Clients → Create client → Client type SAML, Client ID = SP entity ID → Valid redirect URIs = ACS URL → Save.
- Settings: Name ID format
persistent, Force name ID format On, Sign documents On, Sign assertions On. Advanced: Assertion Consumer Service POST Binding URL = ACS URL. - Keys: Client signature required Off unless Sign AuthnRequests is on (then import the SP certificate); Encrypt assertions needs the SP certificate.
- Client scopes →
<client>-dedicated→ Add mapper → By configuration: User Propertyemail→ SAML Attribute Nameemail,firstName→givenName,lastName→surname; Group list → Group attribute namegroups, Single Group Attribute On, Full group path Off. - Magento: IdP Metadata URL
https://<keycloak-host>/realms/<realm>/protocol/saml/descriptor. IdP-initiated: set IDP-Initiated SSO URL name.
Microsoft Entra ID
- Entra admin center → Entra ID → Enterprise apps → New application → Create your own application (non-gallery) → Single sign-on → SAML.
- Basic SAML Configuration: Identifier (Entity ID) = SP entity ID, Reply URL (Assertion Consumer Service URL) = ACS URL (or Upload metadata file).
- Attributes & Claims: Unique User Identifier →
user.objectid, format Persistent; the default emailaddress/givenname/surname claims match the defaults. Add a group claim → Groups assigned to the application (avoids the 150-group overage, which sendsgroups.linkand refuses the login), Source attribute Group ID → map the object IDs in Group to Role Map. - SAML Certificates → App Federation Metadata Url (keep its
?appid=) → Magento IdP Metadata URL. Signing: Sign SAML assertion, SHA-256 (defaults). - Users and groups → assign.
Okta
- Admin Console → Applications → Applications → Create App Integration → SAML 2.0.
- Single sign-on URL = ACS URL (Use this for Recipient URL and Destination URL checked), Audience URI (SP Entity ID) = SP entity ID, Name ID format Persistent.
- Attribute Statements
email→user.email,givenName→user.firstName,surname→user.lastName; Group Attribute Statementsgroups, filter Matches regex.*(or Starts with a prefix). - Sign On tab → Metadata URL → Magento IdP Metadata URL. Assertion Encryption (advanced settings) needs the SP certificate.
- Assignments → groups.
AD FS
- AD FS Management → Relying Party Trusts → Add Relying Party Trust → Claims aware → Import data about the relying party published online (SP metadata URL; "from a file" when the admin is IP-restricted) → access control policy → Finish.
- Claim issuance policy: Send LDAP Attributes as Claims (E-Mail-Addresses → E-Mail Address, Given-Name → Given Name, Surname → Surname, Token-Groups - Unqualified Names → Role); Transform an Incoming Claim (UPN → Name ID, Persistent Identifier). Without a Name ID rule the login is rejected.
- Magento: IdP Metadata URL
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xml. AD FS encrypts assertions automatically when the SP metadata carries a certificate.
IdP-initiated login, 2FA, logout
- IdP-initiated (portal tile) needs Allow IdP-Initiated Login. Risk: login CSRF — an attacker can post their own valid unsolicited assertion into a victim's browser and sign the victim into the attacker's admin account. Keep it off unless the tile is required. Such logins land on the admin startup page whatever the RelayState.
- 2FA: Magento 2FA always applies to SAML logins (Trust IdP MFA is OIDC only).
- Logout stays local; SAML single logout is not supported.
Enforce SSO + break-glass
With Enforce SSO on, every native password login is rejected — the admin form and POST /rest/V1/integration/admin/token alike (enforced on Magento\User\Model\User::authenticate()). All sign-in goes through the IdP.
Even with Enforce SSO off, an admin linked to an IdP identity (created or adopted by SSO) cannot use a password while SSO is enabled, so a local password (e.g. from "Forgot password") does not outlive deactivation at the IdP. Local-only admins keep password login.
For recovery if the IdP is misconfigured, turn on Allow Break-Glass Login and list the local admins in Break-Glass Usernames. Only those usernames keep password login; an empty list means no bypass. There is no URL parameter.
With break-glass off, a lockout is recoverable only by disabling enforce_sso via CLI/DB:
bin/magento config:set dmlab_admin_sso/general/enforce_sso 0
2FA
With Magento_TwoFactorAuth enabled, an SSO login skips Magento 2FA only when Trust IdP MFA is on and the validated ID token carries MFA evidence:
amr(RFC 8176) containsmfaormca, or a possession/biometric factor (otp,hwk,swk,sms,tel,sc,fpt,face, …) plus another method (e.g.["pwd","otp"]); Okta and Entra sendmfaafter an MFA challenge;- or
acrequals one of Trusted ACR Values.
Otherwise (no amr/acr, e.g. Google, or password-only) Magento 2FA still applies. Trust IdP MFA = No always keeps Magento 2FA.
IdP logout
With Sign Out of IdP on Logout on, logging out of an SSO-established admin session also ends the IdP session (OIDC RP-Initiated Logout 1.0): after the local logout the browser goes to the IdP's end_session_endpoint with id_token_hint (the login's ID token, kept in the admin session), client_id and post_logout_redirect_uri. This signs the user out of every app using that IdP session.
Register the admin login URL as a post-logout redirect URI at the IdP: https://<admin-host>/<admin-path>/admin/auth/login/. If the IdP advertises no end_session_endpoint, discovery fails, the active provider changed since login, or the session was a password login, logout stays local.
Architecture
admin-sso is provider-agnostic: it handles admin login, JIT accounts and roles, and delegates the OIDC and SAML protocols to the shared engine sso-core. Provider plugins (admin-sso-okta, -azure, -google) register a preset (discovery URL, groups claim, branding) that fills the Identity Provider dropdown. SAML plugins implement Api\SamlPresetInterface (branding only; protocol settings come from dmlab_admin_sso/saml/*) and di-merge it into Model\Saml\SamlPresetRegistry. The built-in Generic OIDC (from sso-core) and Generic SAML 2.0 (saml) presets cover standard IdPs from config alone; plugins stay the branded path.
How it works
- User clicks "Sign in with SSO" →
adminsso/sso/startbuilds the OIDC auth URL (state + nonce + PKCE; pending state valid 10 minutes, at most 10 per session) via sso-core and the active preset, then redirects to the IdP. - The IdP redirects back to
adminsso/sso/callback, which validatesstateand the response issuer (RFC 9207iss), exchanges the code, and normalizes claims into anIdentityvia sso-core. - The ACL role is resolved from IdP groups; if none resolves, the login is refused here.
- JIT: the admin user is matched by IdP issuer +
sub(stored onadmin_user.dmlab_sso_issuer/dmlab_sso_subject_id, unique together, added atsetup:upgrade), falling back to email — adopted only when the IdP marks it verified (email_verified), otherwise refused — and created if absent (with a suffixed username on any local collision). A matched admin must be active and not locked (lock_expires). The role is assigned and the admin backend session is established like a native login:backend_auth_user_login_successfires andMagento_Securityrecords the session (Admin Account Sharing applies). - If the IdP proved MFA, the session satisfies Magento core 2FA (
Magento_TwoFactorAuth); otherwise the user completes Magento 2FA.
SAML: adminsso/sso/start sends a (optionally signed) AuthnRequest via HTTP-Redirect and binds it to the browser in an encrypted dmlab_admin_sso_saml cookie (10 minutes, 5 newest kept).
The IdP POSTs to adminsso/saml/acs, which checks the response against that request, validates signature, audience, destination and replay via sso-core, and maps attributes into an Identity.
Steps 3–4 then run unchanged (issuer = IdP entity ID, subject = NameID); Magento 2FA always applies.
Identity links and switching IdP
An admin is linked to one IdP identity (issuer + sub). Links created before the issuer was stored are bound to the issuer of their next SSO login. A different issuer (new IdP, tenant or authorization server) never matches an existing link: its users go through the normal email adoption / JIT rules, and email adoption only applies to unlinked admins. Switching a provider between OIDC and SAML changes the subject (NameID vs sub), so clear the links first there too. To move linked admins to a new IdP, clear their link first:
UPDATE admin_user SET dmlab_sso_subject_id = NULL, dmlab_sso_issuer = NULL WHERE user_id IN (...);
License
OSL-3.0 © DMLab. Commercial licensing and support: https://dmlab.work.
