OIDC: The Identity Boundary Behind Login
A practical introduction to OpenID Connect: what problem it solves, when it is worth using, how it fits login and payments, and how it differs from OAuth, SAML, CAS, LDAP, JWT, and passkeys.
I recently went back and cleaned up the login systems around a few small applications. The more I worked through it, the more I felt that most applications do not have that many truly unavoidable problems.
The first one is login: who the user is, how they enter the system, and what identity they have after they enter.
The second one is payment: why the user pays, how much they paid, and what they should receive in the product after paying.
Other features are important, of course. They may decide whether a product has any value at all. But if login and payment are blurry, an application is like a small roadside shop with nobody watching the door and nobody keeping the ledger. It may look busy during the day. The trouble appears when it is time to close the books at night.
In the AI era, these two problems become even more concrete. In the old web, a free tool being clicked a few more times usually meant some bandwidth and server load. In many AI applications, a real use may trigger model calls, credit consumption, and a visible bill. Can anonymous users use it? Who owns the free quota? Where do paid plans and permissions live? If a user comes from another application, are they the same person?
These questions eventually return to a plain word: identity.
OIDC is one standard way to answer it.

Chinese version of this article
What OIDC Is
OIDC stands for OpenID Connect. It is built on OAuth 2.0 and is used for authentication.
In one sentence:
OAuth 2.0 mainly answers: can this client access a resource?
OIDC mainly answers: who is the current logged-in user?
When people say “Log in with Google”, “Log in with GitHub”, or “use the company SSO account”, they are often not talking about OAuth alone. They are talking about identity on top of OAuth. That identity layer is usually OIDC, or at least something very close to it.
The most important output of OIDC is the id_token. It is usually a JWT containing claims like these:
{
"iss": "https://accounts.example.com",
"sub": "acct_123",
"aud": "my-web-app",
"exp": 1780000000,
"email": "user@example.com",
"email_verified": true
}
Three fields matter especially:
iss: who issued this identity result.sub: the stable user identifier at the issuer.aud: which application this identity result is intended for.
After an application receives an id_token, it should not glance at the email address and call the user logged in. It has to verify the signature, verify iss, verify aud, verify the expiration time, and then use iss + sub to find or create its own local user.
That sounds fussy, but it solves a fundamental problem. Identity should not be whatever the frontend says. It should not come from a URL parameter. It should not be guessed independently by each application. Identity is issued by a known authority, and applications verify it by a standard process.
That is the boundary.
Why Not Just Write a Login API
A small application often starts with something like this:
POST /login
email + code
-> returns token
This can work. I do not think every early project needs a grand identity platform before it has users. Many projects do not die because they are insufficiently standard. They die because they build a cathedral before opening the front door.
The problem appears when there is more than one application.
The first application wants email login. The second wants GitHub login. The third wants Google. The fourth needs a mini program login. The admin console needs login too. The CLI also needs login. A separate product wants to reuse the same account.
If every application builds this again by itself, a few things happen quickly:
- A user logs in to application A, then has to register again in application B.
- The same email becomes different users in different products.
- Third-party callbacks, secrets, and state checks are scattered everywhere.
- Email codes, rate limits, risk control, bans, and audit logs are rebuilt several times.
- Later, when unified membership, cross-application entitlements, or account merging becomes necessary, the foundation is already loose.
The login endpoint itself is not complicated. Maintaining identity relationships over time is.
This is where OIDC earns its keep. It does not make the login button prettier. It separates the question of “who is allowed to prove the user’s identity” from the business logic of each application.
A central identity service handles authentication: who the user is, which method they used, whether the email is verified, whether the account is disabled. Each application handles its own business: what this user is called here, what role they have, what plan they bought, how many credits remain, whether they are overdue.
Those two concerns should not be kneaded into one lump.
Central Identity and Local Users
When building unified login, it is easy to run to the other extreme: if there is a central identity service, should all applications share one user table?
My answer is no.
A steadier structure looks like this:
Identity center
identity: acct_123
email: user@example.com
providers: email / google / github
Application A
user_id: 1
auth_issuer: https://accounts.example.com
auth_subject: acct_123
role: admin
plan: pro
Application B
user_id: 58
auth_issuer: https://accounts.example.com
auth_subject: acct_123
credits: 1200
status: active
The identity center answers: this is the same person.
The local user table answers: this is the person’s state inside this application.
That division matters.
Login is usually a global problem. Payment and permissions are often application-specific problems. If someone is a paid member in a writing tool, that does not mean they should have the same quota in an image tool. If someone is an administrator in one console, that does not mean they should be an administrator somewhere else. If the central account is disabled, every application should stop access. But a business-level ban inside one application may not need to affect the user’s use of another product.
If everything is stuffed into the id_token, the token eventually becomes a small database. It looks powerful. It is usually dangerous.
id_token is good for identity facts. It is not a good home for fast-changing business state. Plans, credits, points, orders, roles, ban reasons, and usage limits should normally be read from the application’s own system.

A Minimal Login Flow
OIDC has many terms, but the common web flow can first be understood through Authorization Code + PKCE.
Roughly:
1. The user clicks login in the application.
2. The application creates state, nonce, code_verifier, and code_challenge.
3. The application redirects the user to the identity center's /authorize endpoint.
4. The user logs in at the identity center.
5. The identity center redirects back with a temporary code.
6. The application verifies state.
7. The application exchanges code + code_verifier for tokens at /token.
8. The application verifies id_token.
9. The application finds or creates a local user by iss + sub.
10. The application creates its own session.
Several words are easy to mix up.
code is a one-time temporary ticket. It is not a login session.
id_token is the identity result. It proves who the user is.
access_token is a credential for accessing resources. It should not casually be treated as proof of user identity.
refresh_token is a renewal credential. Whether to issue it, to whom, how long it lives, and how it rotates all need care.
For an ordinary web application, the comfortable shape is often this: the backend handles the callback and token exchange, verifies id_token, then writes its own HttpOnly cookie for the browser. The browser does not need to carry long-lived identity-provider tokens around in frontend code.

A minimal pseudo implementation looks like this:
app.get('/auth/start', (req, res) => {
const state = randomString();
const nonce = randomString();
const verifier = randomString();
const challenge = sha256base64url(verifier);
saveTemporaryLoginState(req, { state, nonce, verifier });
res.redirect(
'https://accounts.example.com/authorize?' +
new URLSearchParams({
response_type: 'code',
client_id: 'my-web-app',
redirect_uri: 'https://app.example.com/auth/callback',
scope: 'openid email profile',
state,
nonce,
code_challenge: challenge,
code_challenge_method: 'S256',
}),
);
});
app.get('/auth/callback', async (req, res) => {
const saved = readTemporaryLoginState(req);
if (req.query.state !== saved.state) {
return res.status(400).send('bad state');
}
const tokens = await exchangeCodeForTokens({
code: req.query.code,
codeVerifier: saved.verifier,
});
const claims = await verifyIdToken(tokens.id_token, {
issuer: 'https://accounts.example.com',
audience: 'my-web-app',
nonce: saved.nonce,
});
const user = await findOrCreateLocalUser({
issuer: claims.iss,
subject: claims.sub,
email: claims.email,
});
writeLocalSessionCookie(res, user.id);
res.redirect('/dashboard');
});
Real projects also need error handling, expiration handling, redirect allowlists, secure cookie attributes, logging, rate limiting, and a few unpleasant edge cases. But the main line is this.
When OIDC Fits
OIDC is useful in several situations.
First, multiple applications need shared login.
Even if there are only two applications today, it is worth thinking about this early. Once login spreads out, pulling it back later is expensive. User data, third-party identities, email verification, old tokens, historical orders, and support records all become migration work.
Second, login and business users should be separated.
This is especially important for products with payments, credits, quotas, teams, roles, bans, or other application-specific state. The identity center should not become a super business-user table. The local user table should not rebuild a full identity-provider system either.
Third, third-party identity providers need to be connected.
Google, Microsoft, enterprise identity, and organization accounts all fit naturally into the OIDC model. Even when a platform document calls the feature “OAuth login”, there is often an id_token or a similar identity-verification step behind it.
Fourth, CLI, mobile apps, and separate services need a common login path.
Web applications can use Authorization Code + PKCE. A CLI can also use PKCE if a browser is available. Without a browser, device code flow may be a better fit. Mobile apps should use the system browser or the platform-recommended approach. Asking users to type passwords into an untrusted WebView is asking for trouble.
Fifth, cross-application membership or account governance may appear later.
Payment does not always belong inside the identity center. But the identity center should at least answer one question stably: which person is this? Without that, orders, entitlements, credits, risk control, and support cases are hard to coordinate across products.
When Not to Rush
Not every project needs OIDC immediately.
If it is a small team tool with one web frontend and a small user base, simple session login may be enough.
If the application is completely tied to one platform, such as a mini program where the user system revolves around that platform’s openid, it may be more practical to make that platform login solid first.
If the problem is service-to-service communication, the question is usually not “who is the user” but “is this service allowed to call that service”. API keys, mTLS, or OAuth client credentials may fit better.
If the goal is to let users avoid passwords, WebAuthn and passkeys are excellent authentication methods, but they do not replace OIDC. A passkey is closer to a way for the user to prove themselves. OIDC is a way for applications to trust the identity result issued by an identity provider.
There is another case worth watching: adopting a heavy identity platform too early just to look professional. The business has not run yet, but callbacks, certificates, clients, realms, and scopes have already wrapped the team into a knot.
Standards are meant to reduce long-term complexity. They should not manufacture complexity on day one.
How It Differs From Other Options
OAuth 2.0 and OIDC are the easiest pair to confuse.
OAuth is about authorization. For example, an application wants to read a user’s cloud drive files. The user grants permission, then the application uses an access token to call the drive API. The key question is: can this client access this resource?
OIDC adds an identity layer on top of OAuth. It standardizes id_token, sub, iss, aud, nonce, Discovery, JWKS, and UserInfo. It lets applications verify who the logged-in user is.
SAML is older and still common, especially in enterprise SSO. It is XML-based and has a mature enterprise software ecosystem. For modern web and mobile applications, though, the developer experience is usually lighter with OIDC.
CAS appears often in universities and older organization systems. It provides straightforward single sign-on, but its ecosystem is not as common as OIDC for modern API and app scenarios.
LDAP and Active Directory are closer to directory services. They are good at storing organizations, users, groups, and credentials, and they are widely used in enterprises. But letting every modern web application connect directly to LDAP is usually rougher than putting an OIDC provider in front.
Session cookies are the most common login state for a single web application. They do not conflict with OIDC. In many cases, the clean design is exactly this: use OIDC for external login, then use an application-owned HttpOnly session cookie for local login state.
JWT is not an alternative to OIDC. JWT is only a token format. Signing your own JWT does not mean you have implemented OIDC. OIDC cares about issuer, audience, discovery, public keys, flows, and verification rules.
WebAuthn and passkeys solve “how the user proves themselves”. OIDC solves “how an application trusts an identity result from an identity provider”. They can work together: the user logs in to the identity center with a passkey, and applications receive the result through OIDC.
Why These Concepts Matter More in the AI Era
AI makes code faster to write. It also makes system design problems show up earlier.
Before, an application could survive for quite a while with one user table and a few endpoints. Now applications connect more easily to each other. Admin consoles, CLIs, agents, mobile apps, independent services, and automation jobs can all become entrances to the same product capability.
AI also lets more people build more of the system by themselves. One person can write frontend, backend, admin pages, deployment scripts, payment flows, and login flows. Once that becomes possible, the dangerous part is not failing to write code. The dangerous part is not knowing where the boundaries should be.
Login is especially like this.
If login is modeled badly, it is not a matter of changing a few pages later. User identity, payment records, permissions, risk control, account merging, deletion, and auditing all get tied together. AI can help write the code. The concepts still need to be understood by the person building the system.
For independent developers and small teams, a few low-level judgments are worth keeping:
- Login is an identity boundary, not just a button.
- Payment is a ledger of business entitlements, not just a webhook.
- The identity center should not swallow the application user table.
- The application user table should not rebuild the identity center.
- More tokens are not automatically better. Larger scopes are not automatically better.
- Who the user is and what the user can do should be modeled separately.
These judgments are not flashy. They are useful.
Finally
OIDC is not something every project must adopt on the first day.
But once there are multiple applications, once the same person should have one identity across products, or once repeated email login, third-party callbacks, verification codes, risk control, and account merging start appearing in different places, it is time to understand OIDC seriously.
It does not solve “how to draw a login page”. It solves “who is allowed to prove who this user is”.
Once that question is clear, many later designs become calmer: the identity center handles authentication, applications handle business state; id_token proves identity, local sessions keep users logged in; iss + sub gives a stable mapping, while payments and permissions stay in each application’s own ledger.
Technical protocols eventually return to ordinary bookkeeping.
Someone has to watch the door. Someone has to keep the accounts. If an application wants to live for a long time, login and payment cannot stay vague.