OAuth2, OIDC, and why you might want your own identity provider
Part 1 of the Be Your Own Identity Provider series.
I have another package to write about: bambamboole/laravel-oidc,
a suite that turns a Laravel app into an OpenID Connect identity provider — and,
separately, into a client of one. Four posts: this one on the why, then one each on
the server, the client, and the UI.
Let me start with the two protocols, because most of the confusion in this space comes from treating them as one thing.
OAuth2 is not login
OAuth2 is an authorization framework. It answers one question: may this application do that thing on someone’s behalf? The answer arrives as an access token, and the whole ceremony — redirects, codes, PKCE — exists to get that token to the right application without it leaking on the way.
What OAuth2 deliberately does not tell you is who the user is. An access token is addressed to the API that will accept it, not to the app that received it. Every “log in with OAuth2” implementation that reads the access token to identify a user is reading someone else’s mail, and that mistake is exactly why the next protocol exists.
OIDC is a thin identity layer on top of OAuth2. It adds:
- an
id_token— a signed JWT that is about the user, addressed to your app (aud), issued by a provider you name (iss), and tied to your specific login attempt (nonce) - a discovery document at
/.well-known/openid-configuration, so a client can configure itself from one URL instead of five - a JWKS endpoint publishing the public keys, so signatures verify without a shared secret and key rotation doesn’t require a redeploy
- userinfo, logout, and a standard vocabulary of scopes and claims
That’s the split worth keeping in your head: OAuth2 answers may this app do that, OIDC answers who is this, and how do I know.
Then MCP made it everyone’s problem
For years, most backend developers could stay out of all this. You had a session
cookie, auth()->user(), and OAuth2 was that thing you touched once when adding
“Sign in with GitHub”.
Then MCP arrived, and its authorization story is OAuth2 — an MCP server is a resource server, its clients get tokens from an authorization server, tokens are audience-scoped, and the whole thing leans on OAuth 2.1’s stricter rules. Suddenly a protocol many of us cheerfully outsourced is sitting in the middle of a feature people want to ship this quarter. “Which authorization server issues the tokens my MCP server accepts?” turns out to be a real architectural decision, and for a lot of Laravel apps the honest current answer is nobody, we never had one.
The vendors are fine, mostly
There is no shortage of people who will run this for you: Auth0, WorkOS, Okta, Entra ID, Keycloak, Zitadel. They are good products, and if you are a startup whose identity needs are a login page and SSO for enterprise customers, buying that is almost certainly the right call. I’m not here to talk anyone out of it.
The reasons I didn’t, in the order they actually matter to me:
Owning the stack. This is the big one. Identity is where the most product-specific logic ends up — a claim derived from your domain model, an extra step for one tenant, a rule about who may authorize what. With a vendor, every one of those becomes a webhook, a rules engine, or a support ticket against their model of a user. In my own app it’s a method. I can adapt it wherever it needs adapting, without asking.
Cost. Per-MAU pricing has a way of turning a solved problem back into a line item right when you’d rather not think about it. Self-hosting an OIDC provider is a fixed cost you already pay for.
Owning the identity data. My users live in my database, and moving off a provider later is a migration I’d rather never plan.
Self-SSO. I run several small apps. I want one login for all of them — and one of those apps is already the natural home for the accounts. Standing up an external vendor to let my apps log into my own app was always the part that felt backwards.
Three packages
So I built it, and split it along the line the protocol already draws:
| Package | What it is |
|---|---|
bambamboole/laravel-oidc-server | The identity provider: protocol endpoints, tokens, and an optional full auth engine |
bambamboole/laravel-oidc-client | The relying party: log users in through any OIDC provider |
bambamboole/laravel-oidc-ui | The auth screens, as Lattice pages |
bambamboole/laravel-oidc ships all three; the split packages install on their own.
The split matters more than it looks: an app that only wants to consume an identity
provider has no business pulling in an authorization server, TOTP and WebAuthn, and
an identity provider has no business forcing a frontend on you.
The OAuth2 core underneath the server is Laravel Passport 13. I extend and reconfigure it rather than reimplementing an authorization server, because writing your own token endpoint is a fine way to find out what you didn’t know about the RFCs.
Next up: the server — what Passport gives you, the sizeable list of things it doesn’t, and what it takes to turn one into an identity provider other applications can actually authenticate against.