OAuth2 & OpenID Connect Deep Dive
"Login with Google" looks like one button but hides a specific, standardized protocol with four distinct actors. This chapter is OAuth2 and OpenID Connect, precisely.
Learning objectives
- Beginner: Name the four actors in OAuth2 and what each one's role is.
- Intermediate: Implement the Authorization Code Grant with PKCE for a web application.
- Advanced: Choose the correct grant type (Authorization Code, Client Credentials, etc.) for a given integration scenario, and explain how a resource server validates the resulting token.
🌱 BEGINNER STORY
When you use valet parking, you don't hand the valet your ENTIRE keyring (house keys, office keys, everything) — you hand them a special VALET KEY that only starts the car and opens the driver's door, nothing else (not the trunk, not the glovebox). This is exactly the problem OAuth2 solves: when 'Sign in with Google' lets a random app see your Google Calendar, you don't want to hand that app your actual Google PASSWORD (your full keyring). Instead, Google gives the app a limited, scoped 'valet key' (an ACCESS TOKEN) that only allows exactly what you approved — say, reading your calendar — nothing more, and you never had to reveal your real password to that third-party app at all.
Before OAuth2 became standard, the common (bad) pattern was: a third-party app asks you for your username/password DIRECTLY for another service (e.g., 'enter your Gmail password so we can import your contacts'). This is dangerous for obvious reasons: the third-party app now has your FULL password (able to do anything your account can do, forever, until you change your password), and you have no fine-grained way to revoke just that one app's access without changing your password everywhere. OAuth2 is an AUTHORIZATION framework (notably NOT an authentication protocol by itself) that lets a user grant a third-party application LIMITED, SCOPED, REVOCABLE access to their resources on another service, without ever sharing their actual credentials with that third-party app.
Figure: The four roles in every OAuth2 flow: Resource Owner, Client, Authorization Server, and Resource Server.
| Actor | Real-World Role |
|---|---|
| Resource Owner | The end user who owns the data/resource (e.g., you, owning your Google Calendar). |
| Client | The application requesting access on the user's behalf (e.g., a calendar-sync app, or your own Angular/mobile app). |
| Authorization Server | Issues tokens after authenticating the user and obtaining their consent (e.g., Google's, or Keycloak, or a Spring Authorization Server you build yourself). |
| Resource Server | The API that actually hosts the protected resource and validates incoming tokens before serving data (e.g., your Spring Boot backend). |
| Term | Meaning |
|---|---|
| Scope | A named, specific unit of access being requested (e.g., 'read:calendar', 'openid', 'profile'). |
| Access Token | The credential the client uses to call the resource server's protected API. |
| Refresh Token | A longer-lived credential used to get a new access token without re-prompting the user. |
| Authorization Code | A short-lived, one-time-use code exchanged for tokens; used in the most common/secure grant type. |
| Client ID / Client Secret | Credentials identifying the CLIENT application itself to the Authorization Server (not the end-user). |
| Redirect URI | The pre-registered URL the Authorization Server sends the user back to after consent, carrying the authorization code. |
| Grant Type | The specific protocol 'recipe' used to obtain tokens (Authorization Code, Client Credentials, Refresh Token, etc.). |
Figure: The Authorization Code grant: the user authenticates directly with the Authorization Server, and the client never sees the password.
This is the flow used for web/mobile apps with a real end-user present. Step by step: (1) the client redirects the browser to the Authorization Server's /authorize endpoint with its client_id, requested scopes, and redirect_uri; (2) the user logs in DIRECTLY on the Authorization Server's own page (the client app never sees the password — this is the entire point); (3) the user approves the consent screen; (4) the Authorization Server redirects back to the client's redirect_uri with a short-lived authorization CODE; (5) the client's BACKEND (never the browser directly, to keep the client_secret confidential) exchanges this code — plus its client_secret — for an access token (and, for OIDC, an ID token) at the /token endpoint; (6) the client can now call the resource server using the access token.
Why the extra 'code' hop instead of returning the token directly in step 4? Because the redirect happens through the BROWSER's URL, which can be logged in browser history, proxy logs, or leaked via the Referer header — a short-lived, single-use code that's immediately exchanged (backend-to-backend, over a channel the browser doesn't see) for the actual token is significantly safer than exposing the powerful access token directly in a URL.
PKCE (Proof Key for Code Exchange, pronounced 'pixy') extends the Authorization Code grant for CLIENTS THAT CANNOT SAFELY KEEP A SECRET — mobile apps and single-page JavaScript apps, where a client_secret embedded in the app is trivially extractable by anyone (decompiling the app, or just reading the JS bundle). Instead of a static secret, the client generates a random 'code_verifier' for each login attempt, derives a 'code_challenge' (a hash of the verifier) sent in step 1, and later proves possession of the original verifier when exchanging the code in step 5 — an attacker who intercepts the authorization code alone (without ever seeing the original code_verifier) cannot complete the exchange. PKCE is now recommended for ALL public clients, and increasingly for confidential clients too, as defense-in-depth.
Used when there is NO end-user at all — one backend SERVICE needs to call another backend service directly (e.g., a batch job, or microservice-to-microservice communication). The calling service authenticates itself directly to the Authorization Server using its own client_id + client_secret and receives an access token representing ITSELF (not any particular user).
# Simplified client credentials token request
curl -X POST https://auth-server/oauth2/token \ -d "grant_type=client_credentials" \ -d "client_id=eazybank-batch-service" \ -d "client_secret=***" \ -d "scope=accounts.read"
| Grant Type | Summary & Status |
|---|---|
| Password Grant (Resource Owner Password Credentials) | The client collects the user's username/password directly and exchanges them for a token. DEPRECATED and discouraged in OAuth 2.1 — defeats the core purpose of OAuth2 (never sharing credentials with the client) and should only ever be considered for fully first-party, highly-trusted legacy migration scenarios. |
| Implicit Grant | An older flow that returned the access token directly in the redirect URL fragment, skipping the code-exchange step, designed originally for pure JavaScript apps that couldn't keep a secret. DEPRECATED in OAuth 2.1 in favor of Authorization Code + PKCE, since tokens in URL fragments are more exposed (browser history, referrer leaks, no proof-of-possession). |
| Refresh Token Grant | Exchanges a previously-issued refresh token for a new access token (and often a new refresh token too), without requiring the user to re-authenticate. |
For JWT access tokens, the resource server fetches the Authorization Server's public signing keys (via a well-known JWKS — JSON Web Key Set — endpoint) ONCE (and caches them), then validates every incoming token's signature LOCALLY — no per-request network call needed. For opaque tokens, the resource server must call the Authorization Server's /introspect endpoint on every request to learn whether the token is valid and what it represents — simpler to revoke instantly, but adds latency and a hard dependency on the Authorization Server's availability for every single API call.
Here's a subtlety worth remembering for interviews: OAuth2 by itself is an AUTHORIZATION framework — it proves 'this client has permission to access X resource,' but technically says nothing formal about WHO the user is. OpenID Connect (OIDC) is a thin identity layer built ON TOP of OAuth2 that adds standardized AUTHENTICATION: it introduces the ID TOKEN (always a JWT, containing standardized identity claims like sub, email, name), the 'openid' scope that triggers its issuance, and a standard /userinfo endpoint for fetching additional profile data. 'Sign in with Google' is OIDC, layered on OAuth2's Authorization Code grant.
| OAuth2 | OpenID Connect (OIDC) | |
|---|---|---|
| Purpose | Authorization (delegated access to resources) | Authentication (proving identity) + Authorization |
| Key token | Access Token (opaque or JWT) | ID Token (always JWT) + Access Token |
| Standard scope | Custom, resource-specific scopes | 'openid' scope triggers ID token issuance |
| Answers the question | 'What can this client do?' | 'Who is this user?' |
🌱 BEGINNER STORY
Remember the valet key from this chapter's opening story? 'Login with Google' is that valet key handed to YOUR OWN application, rather than a third party — your Spring Boot app is the CLIENT, Google is the Authorization Server, and Google's user database is where the Resource Owner (the user) actually lives. Spring Security ships a ready-made module (spring-boot-starter-oauth2-client) that implements the entire Authorization Code + OIDC flow for you — you mostly just need to plug in your registered client_id/client_secret and a couple of configuration lines.
This reuses the exact Authorization Code grant (with OIDC layered on top) already explained in sections 12.5-12.6 — the only difference is that Spring Security's oauth2Login() DSL handles every step (redirect to Google, handle the callback, exchange the code, validate the ID token, and populate an OAuth2AuthenticationToken/OidcUser as the logged-in principal) automatically.
# application.properties
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} spring.security.oauth2.client.registration.google.scope=openid,profile,email @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/", "/login/**", "/error").permitAll() .anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") // your own custom login page .defaultSuccessUrl("/dashboard", true) .userInfoEndpoint(userInfo -> userInfo .oidcUserService(customOidcUserService()))); // optional: map/merge claims into your own User entity return http.build(); } @Component public class LoginController { @GetMapping("/login") public String login() { return "login"; // shows a "Sign in with Google" button linking to /oauth2/authorization/google } }
The link href="/oauth2/authorization/google" is a Spring Security convention — it automatically kicks off the entire Authorization Code flow against Google's Authorization Server for the 'google' registration configured above; you never manually build the /authorize URL or handle the callback yourself. (Note: PKCE is only added automatically for a PUBLIC client, i.e. one registered with no client-secret and client-authentication-method=none — this registration has a client-secret, making it a CONFIDENTIAL client authenticated via client_secret_basic, so PKCE is not part of this particular flow unless you explicitly enable it.)
// Optional: merge the Google-provided profile into your own user record on first login
@Service public class CustomOidcUserService extends OidcUserService { private final CustomerRepository customerRepository; public CustomOidcUserService(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @Override public OidcUser loadUser(OidcUserRequest userRequest) { OidcUser oidcUser = super.loadUser(userRequest); customerRepository.findByEmail(oidcUser.getEmail()) .orElseGet(() -> customerRepository.save( new Customer(oidcUser.getEmail(), oidcUser.getFullName(), "USER"))); return oidcUser; } }
⚠ A real-world nuance worth remembering
The 'social login' user is represented as an OAuth2User/OidcUser principal, which is a DIFFERENT type from the UserDetails principal used by standard username/password login (Chapter 3). If your application supports BOTH login methods (a 'Sign in with email' form AND a 'Sign in with Google' button), your controllers/services that read the current principal need to handle both types — a common real bug is code that assumes Authentication.getPrincipal() is always a UserDetails, which throws a ClassCastException the first time a user logs in via Google instead.
⚠ Production-grade nuances
Using the Implicit or Password grant 'because it's simpler to implement' is a common but discouraged real-world shortcut — modern guidance (OAuth 2.1) removes both entirely in favor of Authorization Code + PKCE for every client type, including SPAs. Forgetting to validate the 'aud' (audience) claim on a JWT access token means a token issued for ONE resource server could mistakenly be accepted by a DIFFERENT resource server that happens to trust the same Authorization Server — always validate the token was actually intended for you. redirect_uri must match EXACTLY (not just 'similar') what's pre-registered with the Authorization Server — a mismatched trailing slash or http vs https is a common source of confusing 'invalid redirect_uri' errors during integration. Treating the ID token (OIDC) as if it were an access token — e.g., sending the ID token in an Authorization header to call an API — is a common confusion; the ID token is meant to be consumed by the CLIENT itself to establish who's logged in, not presented to a resource server as proof of API access. Long-lived refresh tokens that are never rotated or checked for reuse are a real attack surface — production systems increasingly use refresh token ROTATION (issuing a new refresh token on every use, and invalidating the entire family if an old, already-used refresh token is replayed, indicating theft).
Want a visual for this concept?
Generate a diagram tailored to “OAuth2 & OpenID Connect Deep Dive” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →