Single sign-on for CodeIgniter Shield, done carefully
I released Shield OIDC, an open-source package that adds Microsoft Entra ID, Google, Okta, Keycloak and Auth0 sign-in to CodeIgniter Shield. Why it exists, the mistakes it's built to avoid, and how I checked it against each provider's own documentation.
Every internal tool I build reaches the same moment. The app works, people like it, and then someone asks: “Can we just sign in with our Microsoft accounts?” They don’t want another password. IT wants one place to add and remove people, and their own MFA rules applied everywhere.
CodeIgniter’s official auth library, Shield, handles passwords, magic links and API tokens well, but it doesn’t do single sign-on. The closest add-on handles social logins like Google and GitHub by fetching the person’s profile, rather than verifying the provider’s signed ID token, and it doesn’t cover the organizational cases: Entra tenants, roles, who’s allowed in. So I built one, and released it as open source: Shield OIDC.
composer require daemonlabs/shield-oidc
php spark oidc:setup
Add your provider to the config file, put a button on the login page, and people come back from Entra ID, Google, Okta, Keycloak or Auth0 signed in as ordinary Shield users, with their roles mapped to Shield groups if you want. The config builder in Labs writes the config file and walks you through your provider’s setup.
The redirect is the easy part
Sending someone to Microsoft and back takes an afternoon. Deciding who they are when they come back, safely, is where single sign-on goes wrong. These are the traps the package is built around.
Verify the token, don’t just read it
The provider returns an ID token: a signed statement of who signed in. It’s tempting to decode it and use the claims. The package checks the signature against the provider’s published keys first, then the issuer, the audience, the expiry and a one-time nonce, following the OpenID Connect spec step by step. It also refuses the classic forgeries: unsigned tokens, tokens “signed” with the client secret, and keys smuggled into the token itself.
An email address is not an identity
This is the big one. If you match people by email, anyone who can put your CEO’s address on their own account at the provider can sign in as your CEO. With multi-tenant Microsoft apps that’s easier than it sounds: every organization’s administrators can set their users’ email addresses. The attack even has a name, nOAuth.
So people are matched on the provider’s stable ID for them, never the email. An email only counts when the provider vouches for it, and each provider vouches differently:
- Microsoft Entra ID only through its
xms_edovclaim, which says the address’s domain is verified. Microsoft’s own advisory notes that even a single organization’s email field can hold an address someone typed in. - Google only for Gmail addresses, or when the account belongs to a Workspace organization. A personal Google account can carry a work address that was verified years ago by someone who no longer has that job.
- Everyone else through the standard
email_verifiedclaim.
Both sides of a link have to be verified
When someone signs in with SSO for the first time and already has a password account with the same address, it’s convenient to connect the two. But that’s only safe if your app verified the local address too. With Shield’s default open registration, anyone could register a colleague’s address first and wait for that colleague’s first SSO sign-in to land in their account. The package only links automatically when Shield actually confirmed the address; otherwise people connect the provider from their account settings.
Other people’s administrators shouldn’t grant your roles
Mapping a provider role like App.Admin to Shield’s admin group is handy. In a multi-tenant Entra app, though, each organization assigns your app’s roles to its own people, so any customer’s IT admin could make themselves an admin in your app. Roles from other tenants are ignored unless you list the ones you trust.
Checking it against the people who wrote the rules
I started from sign-in code I’d already written for two of my public apps, TicketHub and Seatbookr, hardened it, and wrote tests against a fake identity provider that signs tokens with real keys. Then I tried to break it.
First, a security review from three angles: the cryptography, the sign-in flow and account handling, and the CodeIgniter integration. Then a conformance pass, where every behaviour was checked against primary sources only: Microsoft’s identity platform docs, Google’s, Okta’s, Keycloak’s and Auth0’s, the OpenID Connect and OAuth specifications, and Shield’s own source. I used Claude Code to run those reviews in parallel, the same way I use it to investigate systems with no manual. Findings had to cite their source, I checked the important ones myself before changing anything, and the fixes came with tests that fail without them.
It was worth it. A few of the things that came out of it:
- Google documents that its
email_verifiedflag alone doesn’t prove someone still owns a non-Gmail address. That changed the email rules above. - Google doesn’t list
prompt=loginas a supported way to force a fresh sign-in, and Entra doesn’t documentmax_age. Connecting an account now asks each provider for re-authentication the way that provider documents it. - Keycloak has a client setting that silently stops it from identifying itself on the redirect, which the package (correctly) treats as a possible attack. That’s now in the docs and in the error message.
- Shield stores group names in lowercase. A role mapped to
Admincould be granted but never taken away again. Small detail, real consequence. - The current edition of the OpenID Connect spec tightened how extra audiences in a token are handled, so the package follows the newer rule.
The suite now runs on PHP 8.1 to 8.4 and on the oldest CodeIgniter and Shield versions it supports, and covers the forgeries, the takeover attempts, key rotation and provider outages, and the full sign-in round trip through Shield.
Try it
- Config builder: pick your provider and get the config file,
.envlines and setup steps. - Documentation: provider guides, every option, and the security model in detail.
- Source on GitHub and Packagist, MIT licensed.
It doesn’t do SAML yet, and client authentication is by secret only for now. If you run it against your own provider, I’d like to hear how it went, especially the setups I can’t reproduce on my own desk. And if you’re building an internal tool that needs this kind of sign-in done properly, that’s the kind of work I do.
Have a system your team copies data out of by hand? That’s the kind of thing I build.