daemonlabs / shield-oidc
OpenID Connect single sign-on for CodeIgniter Shield: Microsoft Entra ID, Okta, Google Workspace, Keycloak, Auth0 and any other OIDC provider.
Requires
- php: ^8.1
- ext-curl: *
- ext-json: *
- ext-openssl: *
- codeigniter4/shield: ^1.1
Requires (Dev)
- codeigniter4/framework: ^4.5
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
- codeigniter4/framework: <4.5
Replaces
None
README
Single sign-on for CodeIgniter Shield with any OpenID Connect provider: Microsoft Entra ID (Azure AD), Okta, Google Workspace, Keycloak, Auth0, Authentik, Zitadel and the rest.
People click "Sign in with Microsoft", sign in at their organization's provider, and come back signed in to your app as a normal Shield user, with groups mapped from the provider's roles if you want.
composer require daemonlabs/shield-oidc php spark oidc:setup
Read the full documentation: step-by-step provider guides, every option, account matching, the security model and troubleshooting. (Source in docs/; it's a single page that also works offline.)
Why this package
Shield handles passwords, magic links and tokens. Organizations often want their own sign-in instead: one place to add and remove people, their MFA policies, no extra passwords. Doing that safely takes more than an OAuth redirect:
- The ID token is verified, not just decoded, following OpenID Connect Core 3.1.3.7: signature against the provider's published keys (RS256/384/512, ES256/384/512; weak RSA keys refused), issuer, audience (untrusted extra audiences refused), expiry, nonce and, when a fresh sign-in was asked for,
auth_time.alg: none, HMAC tokens,critheaders and keys embedded in the token are never accepted. - Authorization code flow with PKCE (S256), single-use
state, a per-sign-innonce, and RFC 9207 issuer checks against mix-up attacks. - No account takeover by email. People are matched on issuer + subject. An email only counts when the provider vouches for it in the way that provider documents (Entra's
xms_edov, Google's@gmail.com/hdrule,email_verifiedelsewhere), and existing accounts are only linked when Shield verified their address too. - Entra ID done properly: one tenant (by id or domain), any work account, personal accounts, or a list of allowed tenants; tokens checked against the tenant they came from; other organizations can't grant themselves your app's roles.
- Works the way each provider documents: Entra, Google, Okta, Keycloak and Auth0 behaviour was checked against their official documentation, and the per-provider differences (prompts, claims, logout parameters) are built in.
- Follows Shield: Shield's login,
registerandlogoutevents fire, logins are recorded inauth_logins, pending Shield actions (Email2FA, activation) are completed on Shield's action page as with magic links, and bans are respected. - Keys rotate without downtime, and a short provider outage doesn't stop sign-ins (last known good keys).
- No new dependencies. PHP's OpenSSL extension and CodeIgniter's HTTP client.
php spark oidc:checktests each provider, prints the exact redirect URI to register, and warns about risky settings.
Requirements
- PHP 8.1+ with the
curl,jsonandopensslextensions - CodeIgniter 4.5+ and Shield 1.1+, set up with
php spark shield:setup - HTTPS in production
Setup
-
Install:
composer require daemonlabs/shield-oidc, thenphp spark oidc:setupto createapp/Config/ShieldOidc.php. Routes, commands, helper and messages are found by module discovery. -
Exempt the routes from Shield's global
sessionfilter if you use it: add'oidc/*'to itsexceptlist inapp/Config/Filters.php. -
Register your app with the provider (see the provider guides), add it to
app/Config/ShieldOidc.php, and runphp spark oidc:checkfor the redirect URI to register. -
Show the button on your login page:
<?php helper('oidc') ?> <?= oidc_buttons() ?>
namespace Config; use DaemonLabs\ShieldOidc\Config\ShieldOidc as BaseShieldOidc; class ShieldOidc extends BaseShieldOidc { public array $providers = []; public function __construct() { parent::__construct(); $this->providers['entra'] = [ 'type' => 'entra', 'tenant' => env('ENTRA_TENANT'), // your tenant id or domain 'clientId' => env('ENTRA_CLIENT_ID'), 'clientSecret' => env('ENTRA_CLIENT_SECRET'), ]; $this->providers['google'] = [ 'issuer' => 'https://accounts.google.com', 'clientId' => env('GOOGLE_CLIENT_ID'), 'clientSecret' => env('GOOGLE_CLIENT_SECRET'), 'requiredClaims' => ['hd' => 'yourcompany.com'], // only your Workspace ]; } }
Settings are checked strictly: a misspelled or wrongly typed setting is an error, never silently ignored.
Provider notes
These come from each provider's documentation; the full guides have the click paths.
- Microsoft Entra ID. Keep the
profilescope (Entra sendsoidonly with it). Add theemailandxms_edovoptional claims: withoutxms_edov, Entra emails aren't trusted, even in a single tenant. A Web app needs a client secret. National clouds: setauthority(https://login.microsoftonline.us,https://login.partner.microsoftonline.cn). - Google. Use
requiredClaimshd(or an Internal OAuth audience) to limit sign-in to your Workspace. Google has no provider logout. - Okta. For groups on the org authorization server, add the
groupsscope and a groups claim filter on the app's Sign On tab; on a custom server, add an ID-token groups claim. Register$logoutRedirectunder Sign-out redirect URIs. - Keycloak. Leave the client's Exclude Issuer From Authentication Response off. Use RS256/ES256 signatures. Groups need a Group Membership mapper (values like
/admins); realm roles need a User Realm Role mapper added to the ID token. - Auth0. Use RS256 (apps created by API default to HS256, which is refused). Turn on end-session discovery for provider logout. Roles go in a namespaced claim, e.g.
'groupsClaim' => 'https://example.com/roles'.
Options (summary)
| Option | What it does |
|---|---|
type, issuer, tenant, authority, clientId, clientSecret |
Which provider and app registration |
label, scopes, authParams, redirectUri |
The sign-in request and button |
register, allowedTenants, allowedDomains, requiredClaims |
Who may sign in or get an account |
linkByEmail |
verified (default), trusted or never: linking existing accounts by email |
subjectClaim, usernameClaim, defaultGroup |
How users are identified and created |
groupsClaim, groupMap, groupTenants, syncGroups |
Mapping roles/groups to Shield groups |
logoutAtProvider |
Also end the provider session on /oidc/logout |
loginActions |
Run Shield's login action (e.g. Email2FA) for SSO too |
tokenAuthMethod, trustedAudiences, algorithms, userinfo |
Protocol details for unusual setups |
reauthParams, requireAuthTime |
How a fresh sign-in is requested and checked when connecting an account |
See the options reference for defaults and details.
Accounts and linking
- People are matched on issuer + subject (Entra:
oid). First sign-in creates a Shield user ifregisteris on, with the email only if it's trusted. - An existing account is linked by email only when both addresses are verified: the provider's, and the local account's by Shield (registration closed, or completed email activation). Otherwise people connect the provider from their account with
oidc_link_form('entra', '/account'), a POST form that asks the provider for a fresh sign-in. - Users without an email identity sign in only through the provider and don't appear in Shield's
shield:usercommands.
Security
Please report vulnerabilities privately via GitHub's "Report a vulnerability" (Security tab), not in public issues. See SECURITY.md and the documentation's security section.
Testing
composer install
composer test
The suite runs a fake identity provider with real RSA and EC keys: token forgeries, weak and malformed keys, key rotation and outages, the per-provider rules for Entra and Google, account takeover attempts, Shield actions and events, and the full browser round trip through Shield. The keys in tests/Support/keys are throwaway test keys and aren't part of the installed package.
License
MIT. See LICENSE.