PE
Protocol Explorer
OAuth 2.0

OAuth 2.0 — Refresh Token Grant

Access tokens are intentionally short-lived (minutes to hours). When one expires, the client uses its refresh token to obtain a new access token from the Authorization Server — without requiring the user to re-authenticate. This flow covers the full lifecycle: detecting a 401, silently refreshing, retrying the request, refresh token rotation (a security best practice), and what happens when the refresh token itself expires or is replayed after rotation.

RFC 6749 §6
Web AppAuthorization ServerResource Server1GET /api/profile (expired t…4012POST /token (refresh_token …3GET /api/profile (retry wit…4POST /token (old RT replaye…
GET https://api.example.com/profile401

The client makes an API request with an access token that has expired. The resource server validates the JWT, confirms the signature and claims are otherwise valid, but rejects the request because exp < now. It returns a 401 with WWW-Authenticate indicating error=invalid_token.

RFC 6750 §3.1: The RS MUST respond with HTTP 401 and a WWW-Authenticate header including error=invalid_token when the access token has expired.

The client should check exp before sending requests (clock skew: subtract ~30 seconds) to avoid making a request that will fail. But it must also handle 401 for unexpected expirations.

Short-lived access tokens are a security feature. A stolen token has a limited window of usefulness. The refresh token enables transparent renewal without user friction.

The client should not reveal to the user that the token expired — it should silently refresh and retry the original request.

1 / 4
speed

Step 1: GET /api/profile (expired token → 401)

Request / response
GEThttps://api.example.com/profile
AuthorizationOAuth?

Bearer eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImF1dGgta2V5LTEifQ.eyJpc3…

Accept

application/json

Cryptographic Signature

Construction Steps

1. Why the token is rejected
Access token payload:
{
  "iss": "https://auth.example.com",
  "sub": "user_alice_482",
  "aud": "https://api.example.com",
  "exp": 1699999000,   ← expired at 2023-11-14T23:50:00Z
  "iat": 1699995400,
  "jti": "old-at-123"
}

RS validation sequence:
  1. Fetch AS public key from JWKS → signature OK ✓
  2. iss matches expected AS ✓
  3. aud matches this RS ✓
  4. exp check: 1699999000 < now (1700002600) → EXPIRED ✗

The RS returns 401 with WWW-Authenticate indicating
error=invalid_token. The client must refresh.

Access tokens are kept short-lived (minutes to a few hours)
deliberately. If a token is stolen, it quickly becomes useless.
OAuth Security Best Current Practice (RFC 9700) recommends
keeping access token lifetimes as short as feasible.

Signature Base String

eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImF1dGgta2V5LTEifQ.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyX2FsaWNlXzQ4MiIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNjk5OTk5MDAwLCJpYXQiOjE2OTk5OTU0MDAsImp0aSI6Im9sZC1hdC0xMjMifQ

Signing Key

AS's ES256 key — signature valid, but exp has passed

Signature Output

401 Unauthorized — exp=1699999000 < now=1700002600
Expired Access Tokenat+jwt
Header
{
"typ"?:"at+jwt",
"alg":"ES256",
"kid":"auth-key-1"
}
Payload
{
"iss"?:"https://auth.example.com",
"sub"?:"user_alice_482",
"aud"?:"https://api.example.com",
"exp"?:1699999000,
"iat"?:1699995400,
"jti"?:"old-at-123"
}
sig: mock_old_sig