How OAuth 2.0 Works: The Valet Key Pattern
Stop sharing your password. OAuth 2.0 lets you grant limited access to your data (like 'Read Contacts') without sharing your credentials.
Abstract AlgorithmsAI-assisted content. This post may have been written or enhanced with AI tools. Please verify critical information independently.
TLDR: OAuth 2.0 is an authorization protocol. It lets a third-party app (like Spotify) access your resources (like Facebook Friends) without you giving it your Facebook password. It uses short-lived Access Tokens as scoped, revocable keys.
π The Valet Key Pattern: Access Without Passwords
Your car has two keys:
- Master Key: Opens doors, starts engine, opens trunk and glovebox. (Your Password.)
- Valet Key: Starts the engine and unlocks only the door. Cannot open the trunk. (Access Token.)
You hand the Valet Key to the parking attendant (Third-Party App). They park the car (do the job) but can't steal the contents of the trunk (access your private data).
This is OAuth 2.0 in one sentence: grant limited, temporary access without sharing your password.
π’ The Four Roles Every OAuth Flow Has
| Role | Who It Is | Example |
| Resource Owner | You, the user | The person who owns the Facebook account |
| Client | The app requesting access | Spotify |
| Authorization Server | The identity guard | Facebook Login (accounts.facebook.com) |
| Resource Server | The API holding your data | Facebook Graph API |
The Client never sees your password. It only gets a token from the Authorization Server after you approve the request.
π Tokens, Scopes, and Why Passwords Are the Wrong Tool Here
Before diving into the flow, it helps to understand why OAuth 2.0 was invented. Historically, if Spotify wanted to read your Facebook friends list it would ask for your Facebook username and password. That's catastrophic β Spotify now has full access to your account forever, and you can only revoke it by changing your password.
OAuth 2.0 replaces the password with a token: a short string that proves the user approved access, but only to specific things and only for a limited time.
What a token actually is:
- A random, unguessable string (or a signed JWT β more on that in a follow-up post).
- Issued by the Authorization Server after you click "Allow".
- Bound to specific scopes like
emailorread_friends. - Expires in an hour or less.
- Can be revoked instantly without changing your password.
What a scope is:
A scope is a permission label. When Spotify requests scope=email, it can only read your email. It cannot post on your wall, message your friends, or see your photos. You see exactly what is being requested on the consent screen before you approve anything.
This scoped, time-limited design means that even if a token is stolen, the attacker's window of access is narrow β and you can cut it off immediately by revoking the token in your account settings.
βοΈ The Authorization Code Flow: Step by Step
This is the most secure and most common grant type. Used whenever the Client is a server-side web app.
sequenceDiagram
participant U as User (Browser)
participant C as Client (Spotify)
participant AS as Auth Server (Facebook)
participant RS as Resource Server (FB API)
U->>C: "Login with Facebook"
C->>AS: Redirect: GET /authorize?client_id=...&scope=email
AS->>U: Show consent screen: "Spotify wants your email. Allow?"
U->>AS: Clicks Allow
AS->>C: Redirect back with ?code=AUTH_CODE
Note over C,AS: Server-to-server user never sees this
C->>AS: POST /token {code, client_secret}
AS->>C: {access_token, refresh_token, expires_in}
C->>RS: GET /me/email Authorization: Bearer ACCESS_TOKEN
RS->>C: {email: "you@example.com"}
The sequence diagram reveals the two-phase design that makes the Authorization Code flow secure. The first phase (steps 1β4) runs inside the user's browser: the Client redirects the user to the Auth Server, the Auth Server displays a consent screen, and on approval sends a short-lived Auth Code back through the browser's URL β intentionally exposed because it expires in seconds and is useless without the client_secret. The second phase (steps 5β6) is a direct server-to-server POST: Spotify's backend sends the Auth Code plus its client_secret to Facebook's token endpoint, and receives an Access Token the browser never sees. The final API call (steps 7β8) then uses that Access Token as a scoped bearer credential to fetch exactly the data the user approved.
Why the extra step (Code β Token)?
The Auth Code appears in the browser's URL bar β visible in logs and history. The actual Access Token is exchanged server-to-server where the user/browser never sees it. This prevents token theft via browser history or referrer headers.
π The Authorization Code Flow at a Glance
The sequence diagram above shows the actors. This flowchart shows the decision path β what happens at each fork and what can go wrong:
flowchart TD
A([User clicks Login with Google]) --> B[App redirects browser to /authorize with client_id, scope, redirect_uri, state]
B --> C[Google shows Consent Screen]
C --> D{User approves?}
D -- No --> E([Access Denied redirect back with error])
D -- Yes --> F[Google redirects back to app with short-lived Auth Code in URL]
F --> G[App server POSTs Auth Code + client_secret to /token]
G --> H{Code valid & not expired?}
H -- No --> I([Token Error app must restart flow])
H -- Yes --> J[Google returns Access Token + Refresh Token + expires_in]
J --> K[App calls protected API Authorization: Bearer ACCESS_TOKEN]
K --> L([API returns protected user data])
The flowchart adds the decision branches and failure paths the sequence diagram omits. If the user denies on the consent screen, the flow terminates immediately with an access_denied error redirected back to the app β no Auth Code is ever issued. If the server-side code exchange fails (the code is expired, already used, or the client_secret is wrong), the app must restart the entire flow from step one with a freshly generated state nonce. Both failure paths explain why production implementations validate state on every callback and treat Auth Codes as strictly single-use.
Key insight: The user's browser is only involved up to step F. Everything from the Auth Code exchange onward happens server-to-server, invisible to the browser. That separation is what makes this flow secure.
π§ Deep Dive: Access Tokens, Refresh Tokens, and Scopes
Scopes: Limiting What the Token Can Do
When Spotify redirects you to Facebook, it requests specific scopes:
GET /authorize?scope=email+read_friends
The consent screen shows exactly what Spotify is asking for. You can approve or deny. The token is limited to those scopes β Spotify cannot suddenly read your photos.
Token Lifetimes
| Token | Typical Lifetime | Purpose |
| Access Token | 1 hour | Short-lived bearer credential for API calls |
| Refresh Token | 30β90 days | Exchange for a new Access Token without user interaction |
| Auth Code | 10 minutes | One-time use; expires after the Token exchange |
Refresh Without Re-Login
Client β POST /token { grant_type=refresh_token, refresh_token=... }
Auth Server β new Access Token
The user stays "logged in" without re-approving. The Refresh Token itself is rotated on use in modern implementations (rotation prevents replay attacks).
βοΈ Trade-offs & Failure Modes: Choosing the Right Grant Type
| Scenario | Grant Type | Why |
| Web app with server backend | Authorization Code | Most secure; token never in browser |
| Single-page app (no backend) | Auth Code + PKCE | Replaces client_secret with PKCE verifier |
| Machine-to-machine (no user) | Client Credentials | Service authenticates with its own client_id + secret |
| Highly trusted first-party app | Resource Owner Password | Direct username/password; avoid if possible |
PKCE (Proof Key for Code Exchange): Used for public clients (mobile apps, SPAs) that cannot safely store a client_secret. The client generates a code_verifier and code_challenge pair β the server verifies the proof without needing a pre-shared secret.
OAuth 2.0 vs OpenID Connect
OAuth 2.0 answers: "Can this app access this resource?"
OpenID Connect (OIDC) adds: "Who is this user?" β it returns an ID Token (JWT) with the user's identity alongside the access token. Most "Login with Google" buttons use OIDC on top of OAuth 2.0.
π§ Decision Guide: Which OAuth Grant Type to Use
- Web app with server backend: Authorization Code flow.
- SPA or mobile app: Authorization Code + PKCE.
- Service-to-service (no user): Client Credentials.
- Need user identity: Add OpenID Connect on top.
Avoid the Password grant β it requires your app to handle user credentials directly.
π Client Credentials Flow (Machine-to-Machine)
sequenceDiagram
participant S as Service (Client)
participant AS as Auth Server
participant RS as Resource Server
S->>AS: POST /token {client_id, client_secret, grant_type=client_credentials}
AS-->>S: {access_token, expires_in}
S->>RS: GET /api/data Authorization: Bearer ACCESS_TOKEN
RS-->>S: 200 {data}
Note over S,AS: No user involved service authenticates itself
Unlike the Authorization Code flow, the Client Credentials flow has no user, no browser redirect, and no consent screen. The Service authenticates directly to the Authorization Server using its own client_id and client_secret, and receives an Access Token in a single round-trip β there is no human in the loop to approve anything. The Service then presents that token as a bearer credential on every call to the Resource Server, making this the correct grant type for machine-to-machine scenarios like a billing service calling a payments API at midnight or a CI pipeline fetching deployment secrets.
π‘οΈ Common Security Pitfalls
| Pitfall | Why It Matters | Mitigation |
| Token in URL fragment | Visible in browser history and server logs | Always exchange Auth Code server-side |
Missing state parameter | Enables CSRF attacks during the redirect | Generate and validate a random state nonce |
| Long-lived access tokens | Stolen tokens stay valid longer | Short expiry (1h) + refresh token rotation |
| Overly broad scopes | Exposes more data than needed | Request minimum scopes; show clear consent |
π Real-World Applications: Where You See OAuth 2.0 Every Day
OAuth 2.0 is not an abstract protocol β it runs silently behind nearly every "Login withβ¦" button you have ever clicked.
"Login with Google" on any website. When you click that button, the website (Client) sends you to Google's Authorization Server. You see a consent screen ("This site wants to see your name and email address"). You click Allow. Google sends the site a token. The site calls the Google People API to fetch your profile. You never typed your Google password into the third-party site.
Spotify reading your Facebook friends. Spotify requests scope=user_friends. Facebook shows you a consent dialog listing exactly what Spotify will access. You can allow the friends list but deny access to your photos. Spotify gets a token scoped to just what you approved.
Mobile apps calling APIs. The Twitter mobile app uses OAuth 2.0 with PKCE to get a token that lets it post tweets on your behalf. Because the app can't safely store a client_secret, PKCE replaces it with a cryptographic proof. The token expires in an hour; the refresh token silently renews it so you stay logged in.
Machine-to-machine services. A billing microservice that needs to call a payments API at midnight β no user involved β uses the Client Credentials grant. The service authenticates with its own client_id and client_secret and gets a token scoped only to the payments endpoint. No human consent screen required.
In every case the pattern is the same: the resource owner (you, or an admin who pre-approved) grants a scoped, revocable token to a client β without sharing the master credential.
π§ͺ Implementing OAuth 2.0: Your First Integration and Common Mistakes
If you are adding "Login with Google" to a web app for the first time, here is the practical path:
Step 1 β Register your app. Go to the Google Cloud Console (or any provider's developer portal) and create an OAuth client. You will receive a client_id (public) and client_secret (keep this private β never commit it to git).
Step 2 β Build the redirect URL. Your login button should redirect the user to:
https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&response_type=code
&scope=openid email profile
&state=RANDOM_NONCE
Step 3 β Handle the callback. Google redirects back to /callback?code=AUTH_CODE&state=.... Verify the state matches what you sent (CSRF protection), then exchange the code for a token server-side.
Step 4 β Exchange the code for a token. POST to Google's token endpoint with the code and your client_secret. Store the resulting tokens securely β access token in memory, refresh token in a server-side database.
Common beginner mistakes to avoid:
- Storing the client_secret in frontend code or git. It is a private credential. Use environment variables.
- Skipping the
stateparameter. Without it, an attacker can trick a user into linking their account to the attacker's session (CSRF). - Using too broad scopes. Only request what you actually need. Users see the consent screen β asking for everything looks suspicious and reduces conversion.
- Forgetting token expiry. Access tokens expire. Build in refresh token logic from day one, or your app will fail silently after an hour.
π οΈ Spring Security OAuth2 & Keycloak: Protecting a Spring Boot API with Bearer Tokens
Spring Security OAuth2 Resource Server adds bearer-token validation to any Spring Boot application in two lines of configuration β no manual JWT parsing or signature verification code. When a request arrives with Authorization: Bearer <token>, Spring Security fetches the Authorization Server's JWKS endpoint, validates the token signature, and populates the SecurityContext automatically.
Keycloak is the most popular open-source Authorization Server implementing the full OAuth 2.0 + OIDC spec. It provides a user management UI, realm-based multi-tenancy, and can run locally in Docker for development β making it a drop-in test implementation of the Authorization Server role described in this post.
// build.gradle:
// org.springframework.boot:spring-boot-starter-security
// org.springframework.boot:spring-boot-starter-oauth2-resource-server
// ββ Security configuration βββββββββββββββββββββββββββββββββββββββββββββββββββββ
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated() // every other endpoint needs a valid token
)
.oauth2ResourceServer(oauth2 ->
// Spring fetches the Authorization Server's JWKS endpoint,
// verifies the token signature with the public key, then populates SecurityContext.
// No manual JWT parsing β the issuer-uri in application.yml drives discovery.
oauth2.jwt(Customizer.withDefaults())
)
.build();
}
}
// ββ Controller that inspects JWT claims β shows the OAuth2 token anatomy ββββββββ
@RestController
@RequestMapping("/api")
public class OrderController {
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
// @PreAuthorize checks the "scope" claim inside the JWT.
// Spring Security maps JWT scope β SCOPE_<value> authorities automatically:
// scope="orders:read profile:read" β [SCOPE_orders:read, SCOPE_profile:read]
@GetMapping("/orders")
@PreAuthorize("hasAuthority('SCOPE_orders:read')")
public List<Order> getOrders(JwtAuthenticationToken auth) {
// ββ What's actually inside the JWT the client sent ββββββββββββββββββ
Jwt jwt = auth.getToken();
String sub = jwt.getSubject(); // "sub" β Resource Owner's stable identity
String iss = jwt.getIssuer().toString(); // "iss" β Authorization Server that issued this token
Instant exp = jwt.getExpiresAt(); // "exp" β when this access token expires (short-lived)
String aud = jwt.getAudience().toString(); // "aud" β which Resource Server this token is for
List<String> scopes = jwt.getClaimAsStringList("scope"); // "scope" β permissions the user consented to grant
// Example token payload (decoded):
// { "sub": "user:42", "iss": "https://auth.example.com",
// "exp": 1709985600, "aud": ["orders-api"],
// "scope": "orders:read profile:read" }
log.debug("Token: iss={} sub={} scopes={} exp={}", iss, sub, scopes, exp);
// "sub" is the OAuth2 Resource Owner ID β use it, not a session cookie
return orderService.findByUser(sub);
}
@PostMapping("/orders")
@PreAuthorize("hasAuthority('SCOPE_orders:write')")
public Order createOrder(@RequestBody OrderRequest req, JwtAuthenticationToken auth) {
// Attach the Resource Owner's identity ("sub") to the new order for ownership checks
return orderService.create(req, auth.getToken().getSubject());
}
}
# application.yml β point Spring Security at your Keycloak realm
spring:
security:
oauth2:
resourceserver:
jwt:
# Spring fetches JWKS from this URL to verify token signatures automatically
issuer-uri: http://localhost:8080/realms/my-realm
# Run Keycloak locally with Docker β acts as the Authorization Server from this post
docker run -p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:24.0 start-dev
The SCOPE_orders:read prefix in @PreAuthorize maps directly to the scope concept from this post. Spring Security extracts the JWT's scope claim and creates SCOPE_<value> authorities automatically β so scope=orders:read in the token becomes the SCOPE_orders:read authority enforced by the annotation.
For a full deep-dive on Spring Security OAuth2 Resource Server, Keycloak realm configuration, and JWT claim customization, a dedicated follow-up post is planned.
π Lessons Every Developer Learns from Their First OAuth Integration
OAuth 2.0 has a reputation for being confusing. These are the lessons that clear it up:
OAuth 2.0 is authorization, not authentication. It answers "Can Spotify read my email?" β not "Is this really Spotify?" or "Is this really you?" For user identity, you need OpenID Connect (OIDC) layered on top.
Tokens are not magic β treat them like passwords. An access token in the wrong hands is as dangerous as a password, for the duration of its validity. Store them securely. Never log them. Never put them in URLs.
The consent screen is the user's control panel. This is the moment users decide what access to grant. Design your scope requests to be minimal and honest. Users who trust the consent screen are users who stay.
Short-lived tokens are a feature, not a bug. An hour-long access token limits the blast radius of a breach. Pair it with a refresh token to keep sessions smooth without sacrificing security.
PKCE is not optional for public clients. If your app cannot safely store a client_secret (mobile app, single-page app), use PKCE. It is mandatory in OAuth 2.1 (the upcoming revision of the spec).
π TLDR: Summary & Key Takeaways
- OAuth 2.0 = authorization, not authentication. "Can Spotify see my email?" not "Is this Spotify?" or "Is this really you?"
- Authorization Code Flow: redirect β consent screen β auth code β server-side token exchange β access token.
- Access Tokens are scoped and short-lived (β1 hour); Refresh Tokens allow silent renewal without re-prompting the user.
- PKCE replaces
client_secretfor public clients (mobile apps, SPAs) β and is required in OAuth 2.1. - OpenID Connect (OIDC) adds user identity (
id_token) on top of OAuth 2.0; most "Login with Google" buttons use OIDC. - Always validate the
stateparameter on the callback to prevent CSRF attacks. - Request the minimum scopes your app actually needs β users trust transparent consent screens.
π Related Posts
Test Your Knowledge
Ready to test what you just learned?
AI will generate 4 questions based on this article's content.

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
RAG vs Fine-Tuning: When to Use Each (and When to Combine Them)
TLDR: RAG gives LLMs access to current knowledge at inference time; fine-tuning changes how they reason and write. Use RAG when your data changes. Use fine-tuning when you need consistent style, tone, or domain reasoning. Use both for production assi...
Fine-Tuning LLMs with LoRA and QLoRA: A Practical Deep-Dive
TLDR: LoRA freezes the base model and trains two tiny matrices per layer β 0.1 % of parameters, 70 % less GPU memory, near-identical quality. QLoRA adds 4-bit NF4 quantization of the frozen base, enabling 70B fine-tuning on 2Γ A100 80 GB instead of 8...
Build vs Buy: Deploying Your Own LLM vs Using ChatGPT, Gemini, and Claude APIs
TLDR: Use the API until you hit $10K/month or a hard data privacy requirement. Then add a semantic cache. Then evaluate hybrid routing. Self-hosting full model serving is only cost-effective at > 50M tokens/day with a dedicated MLOps team. The build ...
Watermarking and Late Data Handling in Spark Structured Streaming
TLDR: A watermark tells Spark Structured Streaming: "I will accept events up to N minutes late, and then I am done waiting." Spark tracks the maximum event time seen per partition, takes the global minimum across all partitions, subtracts the thresho...
