OAuth2 and OpenID Connect
Authorization code with PKCE, client credentials, the roles of each party, and why a backend rarely needs the password grant.
OAuth 2.0 is an authorisation framework: it lets one application act on a user's behalf at another, without the user handing over their password. OpenID Connect is a thin layer on top that adds authentication — telling you who the user is.
That distinction is the one to hold, because the two are constantly conflated, and the whole of OAuth makes more sense once you notice that plain OAuth 2.0 never actually tells you who anybody is.
Four parties, and naming them correctly
Almost every OAuth confusion is a party confusion:
| Party | Who it is | In a typical Java system |
|---|---|---|
| Resource owner | the human | your user |
| Client | the thing wanting access | your web or mobile front end |
| Authorization server | issues tokens | Keycloak, Auth0, Okta, Entra ID |
| Resource server | holds the data | your Spring Boot API |
Note the last row. In most backend work you are the resource server — you receive and validate tokens, and you do not issue them. The previous lesson was that job in full. Everything below is about how the token got made.
Note also that the "client" is not your backend. It is the thing the user is using.
The authorization code flow, with PKCE
This is the flow. The others are special cases or deprecated.
- The user clicks Sign in. The client redirects the browser to the authorization server, with its
client_id, thescopeit wants, aredirect_uri, a randomstate, and acode_challenge. - The user authenticates at the authorization server — the client never sees the password, and that is the entire point of the exercise.
- The authorization server redirects back to
redirect_uriwith a short-lived authorization code. - The client exchanges that code for tokens, sending the
code_verifierthat matches the challenge from step 1. - The client receives an access token, usually a refresh token, and — with OIDC — an ID token.
Two of those parameters are security mechanisms rather than plumbing:
stateis a random value the client generates and checks on return. It ties the response to the request the client actually started, which is CSRF protection for the login itself.- PKCE (
code_challenge/code_verifier) proves the party redeeming the code is the party that requested it. It was designed for mobile apps that cannot keep a secret, and it is now recommended for every client, including confidential ones. Use it always.
The reason for the two-step dance — a code, then an exchange — is that the code travels through the browser, where it lands in history and logs, while the token is fetched over a direct back-channel call. A code is useless without the verifier and expires in seconds.
The grants, and which to use
| Grant | Use it |
|---|---|
| Authorization code + PKCE | anything with a user: web, mobile, SPA |
| Client credentials | service to service, no user involved |
| deprecated — returned tokens in the URL fragment | |
| deprecated — the client handles the password, defeating the point | |
| Device code | TVs and CLIs, where typing a password is impractical |
| Refresh token | exchanging a refresh token for a new access token |
Client credentials is the one a backend engineer uses most after being a resource server. Your batch job calls another service; there is no user; the job authenticates as itself with a client id and secret and gets a token scoped to what it may do. Least privilege from the authorisation lesson, applied to a machine.
The two deprecated rows are worth recognising in an old codebase. The password grant in particular looks convenient — the app collects a username and password and exchanges them — and it reintroduces exactly the problem OAuth exists to remove: an application handling credentials it has no business seeing.
OIDC: the ID token, and what each token is for
OpenID Connect adds an ID token: a JWT about the user, for the client.
Three tokens, three jobs, and mixing them up is a real class of bug:
| Token | Audience | Says | Send it to your API? |
|---|---|---|---|
| Access token | the resource server | what the bearer may do | yes |
| ID token | the client | who the user is, and how they authenticated | no |
| Refresh token | the authorization server | may be exchanged for a new access token | no |
The ID token is for the client to learn who signed in — a name to show, a subject id to key on. Sending it to an API as a bearer token is a common mistake: it was issued with the client as its audience, so a correctly configured resource server rejects it, and an incorrectly configured one accepts a token that was never meant for it. This is the aud check from the previous lesson, and this is what it is defending against.
An access token may or may not be a JWT, incidentally. It can be an opaque string, in which case the resource server validates it by calling the authorization server's introspection endpoint — a network call per request, with revocation that is immediate. JWT or introspection is the same stateless-versus-stateful trade as sessions and tokens, one layer up.
Scopes are not permissions
A scope says what the user consented to let this client do — orders:read. It is not the same as what the user is allowed to do.
A token with orders:read does not mean the holder may read every order. It means the client is permitted to ask about orders on this user's behalf; which orders is still your authorisation decision, from the object and its owner.
Treating scope as authorisation reproduces exactly the broken-object-level-authorisation bug from the fundamentals lesson, with a token in front of it.
As a resource server, in practice
For most Java backends the work is small, because the previous lesson is the whole job:
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.comPlus an audience check, a claims-to-authorities mapping, and ownership checks in your services. Do not build an authorization server. Issuing tokens, storing consent, rotating keys, implementing the flows and keeping up with the specifications is a product, and there are several good ones — Keycloak if you want to run it yourself, a hosted provider if you do not.