August 2, 2026

OAuth2, OIDC, and why you might want your own identity provider

By Manuel Christlieb — Staff Engineer

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:

PackageWhat it is
bambamboole/laravel-oidc-serverThe identity provider: protocol endpoints, tokens, and an optional full auth engine
bambamboole/laravel-oidc-clientThe relying party: log users in through any OIDC provider
bambamboole/laravel-oidc-uiThe 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.