The mental model

OAuth roles, the Authorization Code flow, PKCE, the three kinds of token, and JWT signing, before any code.

  • Part 1
  • beginner
  • about 25 minutes, reading only

You will build

a working model of who trusts whom, with which credential, for what

You will understand

the four OAuth roles, why PKCE exists, how access, refresh and ID tokens differ, scopes versus roles, and why a JWT cannot simply be revoked

Before writing a security configuration, we need a model that lets us answer a simple question whenever the code becomes confusing:

Which party is trusting which other party, for what purpose, and based on which credential?

That question is more useful than memorizing endpoint names.

The four OAuth roles

A typical OAuth deployment has four conceptual roles:

flowchart LR
    RO[Resource Owner]
    C[Client]
    AS[Authorization Server]
    RS[Resource Server]

    RO -->|uses| C
    C -->|asks for authorization| AS
    AS -->|issues access token| C
    C -->|access token| RS
  • Resource Owner — usually the human user.
  • Client — the application trying to access something.
  • Authorization Server — authenticates/authorizes and issues tokens.
  • Resource Server — API that accepts access tokens.

One physical application can play more than one role. In our project, the authorization server also protects its own admin REST endpoints as a resource server.

Authorization Code flow

For a user-facing application, the flow we care about is Authorization Code.

sequenceDiagram
    participant U as User
    participant B as Browser
    participant C as Client
    participant AS as Authorization Server

    U->>C: Open application
    C->>AS: /authorize request
    AS->>U: Authenticate + consent
    AS-->>C: authorization_code
    C->>AS: code + client auth + verifier
    AS-->>C: access_token (+ refresh/id token)

The browser never receives the final client secret exchange as part of the front-channel redirect. Instead, the authorization response carries a short-lived authorization code, which is redeemed at the token endpoint.

Why PKCE exists

PKCE is defined by RFC 7636. It binds an authorization request to the party that later redeems the code.

The client creates a random code_verifier and derives a code_challenge:

code_verifier
      |
      | SHA-256
      v
code_challenge

The authorization request sends the challenge:

GET /oauth2/authorize
    ?response_type=code
    &client_id=...
    &code_challenge=...
    &code_challenge_method=S256

The token request sends the original verifier:

POST /oauth2/token

grant_type=authorization_code
code=...
code_verifier=...

The authorization server hashes the verifier and compares it with the challenge saved with the authorization request.

An attacker who intercepts only the authorization code cannot redeem it without the verifier.

Generate a PKCE pair manually

We will use this throughout the series:

CODE_VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
CODE_CHALLENGE=$(
  printf '%s' "$CODE_VERIFIER" \
  | openssl dgst -sha256 -binary \
  | openssl base64 -A \
  | tr '+/' '-_' \
  | tr -d '='
)

Public versus confidential clients

A confidential client can keep a credential secret—for example, a server-side application.

A public client cannot reliably hide a long-term secret—for example, a browser SPA or installed mobile application.

flowchart TD
    C{Client can safely hold a secret?}
    C -->|Yes| Conf[Confidential client]
    C -->|No| Pub[Public client]
    Conf --> Secret[client_secret_basic etc.]
    Pub --> None[client authentication method: none]
    Pub --> PKCE[PKCE is essential]

Our management API will eventually support both types.

Access token, refresh token, ID token

These are different credentials with different audiences.

Access token

An access token represents delegated authorization.

The resource server asks:

Is this token valid and does it contain the authority/scope required for this API?

Refresh token

A refresh token is presented to the authorization server to obtain a new access token without repeating the full browser authorization flow.

Because it can extend a session, it deserves stronger handling than an ordinary short-lived access token. RFC 9700 discusses refresh-token protection and rotation as an important modern security practice.

ID Token

An ID Token belongs to OpenID Connect. It is an identity assertion consumed by the client.

It answers questions such as:

Who authenticated?
Which issuer authenticated them?
Which client was the token intended for?
When was it issued and when does it expire?

Do not use the ID Token as a substitute for an access token when calling APIs.

OAuth scopes versus application roles

We will keep two concepts separate:

OAuth scope: read, write, orders.read
Application role: USER, ADMIN

A scope describes delegated access granted to a client.

A role describes privileges of the authenticated application user in our own authorization-server application.

Our JWT access token will eventually include both:

{
  "scope": ["openid", "profile", "read"],
  "roles": ["USER", "ADMIN"]
}

That does not make scopes and roles interchangeable.

What OpenID Connect adds

OpenID Connect is an identity layer on top of OAuth.

When the authorization request includes:

scope=openid

we are asking for an OpenID Connect authentication flow.

Important OIDC pieces include:

openid scope
ID Token
UserInfo endpoint
standard claims
provider discovery

Later we will expose preferred_username for profile and email for the email scope.

JWT and signing keys

Our access tokens will be self-contained JWTs.

A JWT has three base64url-encoded parts:

header.payload.signature

For example:

{
  "alg": "RS256",
  "kid": "..."
}
{
  "iss": "http://localhost:9000",
  "sub": "user",
  "aud": "demo-client",
  "scope": ["openid", "read"],
  "roles": ["USER"]
}

The authorization server signs with its private key. Consumers verify with the public key, normally obtained through the JWKS endpoint.

flowchart LR
    Private[Private RSA Key] -->|sign| JWT[JWT]
    JWT --> API[Resource Server]
    JWKS[Public JWK / JWKS] -->|verify| API

Never distribute the private key to resource servers.

Self-contained JWTs and revocation

JWTs create an important tradeoff.

A resource server can validate a signed JWT locally without calling the authorization server on every request. That is fast and scalable.

But if the authorization server marks the token revoked in its database, a purely local verifier does not automatically know that. The token’s cryptographic signature is still valid until it expires.

Later we will use two approaches:

  1. keep access tokens short-lived;
  2. make our sensitive management APIs also check server-side authorization state.

For other resource servers, introspection is another option.

Trust boundaries for our project

flowchart TB
    Internet[Untrusted network]
    Browser[Browser]
    AS[Authorization Server]
    DB[(PostgreSQL)]
    Keys[Private signing key]

    Internet --> Browser
    Browser --> AS
    AS --> DB
    AS --> Keys

    classDef critical stroke-width:3px;
    class DB,Keys critical;

The most sensitive assets include:

  • user password hashes;
  • refresh tokens/authorization state;
  • confidential-client secrets;
  • private signing keys;
  • administrative bearer tokens.

The tutorial’s later design choices—profile-isolated seed data, key files outside Git/Docker images, admin bearer authentication, audit logging—follow from these boundaries.

Modern OAuth security direction

RFC 9700 is especially useful because it captures modern security best practices learned after the original OAuth 2.0 RFC. Our server follows that general direction by using Authorization Code + PKCE, avoiding the implicit flow, rotating refresh tokens, and invalidating authorization state on password changes.

OAuth 2.1 is still a working-group Internet-Draft in September 2026, so think of it as consolidation of the modern OAuth model rather than a finished RFC at the time this tutorial was written.

Checkpoint

You should now be able to explain:

  • the difference between client, authorization server, and resource server;
  • why Authorization Code does not return the access token through the front channel;
  • what PKCE binds together;
  • why access, refresh, and ID tokens are different;
  • the difference between roles and scopes;
  • why JWT revocation is not automatically visible to an offline verifier.

If any of those are unclear, revisit them now. The Spring Security code in later parts will make much more sense once these concepts are stable.

References

Next: bootstrap Spring Boot and expose our first real authorization-server endpoints.