Understanding OAuth 2.0 and OpenID Connect by Building a Minimal System

Over the past few days, I set out to understand OAuth 2.0 and OpenID Connect (OIDC) beyond surface-level usage. Instead of relying on libraries, I tried to build a minimal version of the flow myself.
This approach was slower than expected, and at times frustrating, but it helped me connect the pieces into a coherent system rather than a collection of endpoints.
This article summarizes that understanding.
Rethinking OAuth: It’s Not Just “Login”
A common simplification is:
“OAuth is used for login (e.g., Login with Google)”
While that’s a popular use case, it’s not the core idea.
OAuth is fundamentally about:
granting controlled access to resources on behalf of a user
OpenID Connect builds on top of OAuth to provide:
identity information about the user
Key Actors in the System
Any OAuth/OIDC flow involves three primary components:
Resource Owner (User) — the person whose data is being accessed Client — the application requesting access Authorization Server — the system that authenticates users and issues tokens
In many implementations, the authorization server and resource server may be separate, but in a minimal system, they can be combined.
Step 1: Client Registration
Before participating in the flow, a client must be registered with the authorization server.
The server issues:
client_id — public identifier client_secret — confidential credential
These are used later to authenticate the client during token exchange.
Step 2: User Authentication
The user signs up or logs in through the authorization server.
At this stage:
The system knows who the user is No permissions have yet been granted to any client
Authentication and authorization are intentionally separate concerns.
Step 3: Authorization Request
When the user initiates “Login with X”, the client redirects the user’s browser to the authorization server:
/authorize?client_id=...&redirect_uri=...&scope=...&state=...
The authorization server performs:
Client validation Session check (is the user logged in?)
If the user is not authenticated, they are redirected to the login flow first.
Step 4: User Consent
Once authenticated, the user is presented with a consent screen indicating what the client is requesting (defined by scope).
The user can:
Approve the request Deny the request
This step is central to OAuth’s permission model.
Step 5: Authorization Code Issuance
If the user approves, the authorization server:
Generates a short-lived authorization code Stores it with: userId clientId scope redirectUri expiration time Redirects the browser back to the client: redirect_uri?code=...&state=...
Important:
The authorization code is not an access token. It is an intermediate credential.
Step 6: Token Exchange (Server-to-Server)
This is a critical distinction in the flow.
After receiving the code, the client’s backend (not the browser) sends a request to the authorization server:
POST /token
With:
client_id client_secret code redirect_uri
This is a secure, server-to-server interaction.
Step 7: Validation and Code Exchange
The authorization server validates:
Client credentials (client_id and client_secret)
Authorization code existence and integrity
Matching redirect_uri
Code expiration
Client association (code must belong to that client)
If valid:
The authorization code is invalidated (one-time use)
Step 8: Token Issuance
The server responds with:
access_token
id_token (in OpenID Connect)
metadata such as expires_in and token_type
The access token is used to access protected resources.
The id_token contains identity claims about the user (e.g., subject, email).
Step 9: Accessing Protected Resources
The client backend uses the access token to call protected endpoints:
GET /userinfo Authorization: Bearer <access_token>
The resource server:
verifies the token returns user information
The client can then establish its own session for the user.
A Mental Model That Helped :-
One distinction simplified the entire flow:
/authorize → involves the user and browser /token → involves server-to-server communication
Understanding this separation clarified why the authorization code exists and why tokens are not returned directly in the browser.
Building a minimal version of OAuth 2.0 and OpenID Connect helped me realize that the real complexity doesn’t lie in individual endpoints, but in how they work together as a system.
There were moments where progress felt slow, mostly because my understanding of the overall flow wasn’t clear yet. Once that mental model started to form, the implementation became much more straightforward.
This is still a simplified interpretation, and there’s more to explore—especially around security and production-grade considerations—but working through the fundamentals has made the concepts far more intuitive.
Approaching it this way may take more time than using existing libraries, but it builds a level of clarity that is difficult to achieve otherwise.
