Skip to content
pwnsy
network-securityintermediate#oauth#authorization#identity#api-security#tokens

OAuth 2.0 Explained: Roles, Grant Types, and Tokens

A plain guide to OAuth 2.0: the four roles, the grant types and when to use them, access and refresh tokens, and how to deploy it safely.

OAuth 2.0 is the framework behind almost every "Sign in with" button and every app that connects to your Google, Microsoft, or GitHub account. Its job is narrow and useful: let one application do something on your behalf in another service, with your consent, without you handing over your password to that application.

That last point is the whole reason OAuth exists. Before it, sharing access meant giving an app your actual credentials, which meant that app could do anything you could, forever, with no way to limit or revoke it cleanly. OAuth replaces the password with a scoped, revocable, time-limited token. The framework is defined in RFC 6749.

One clarification up front, because it causes endless confusion. OAuth 2.0 is an authorization framework: it grants an application permission to access resources. It is not, by itself, an authentication protocol for logging users in. The login use case is handled by OpenID Connect, which is a thin identity layer built on top of OAuth. Keeping that distinction straight prevents a lot of insecure designs.

The four roles

Every OAuth flow is a conversation between four parties. Name them clearly and the grant types stop being confusing.

RoleWho it isResponsibility
Resource OwnerThe userOwns the data and grants access to it
ClientThe applicationWants to access the resource on the user's behalf
Authorization ServerThe identity providerAuthenticates the user and issues tokens
Resource ServerThe API holding the dataAccepts tokens and serves protected resources

A concrete example: you (resource owner) let a photo-printing app (client) access your cloud photos. The cloud provider's login system (authorization server) checks who you are and asks your consent, then issues a token the printing app presents to the photo API (resource server) to fetch your images.

The client is further split into two kinds, and this drives which grant type is safe:

  • Confidential clients can keep a secret, such as a server-side web app with a backend.
  • Public clients cannot keep a secret, such as a single-page app or a mobile app, where anything shipped to the device can be extracted.

The tokens

OAuth runs on tokens, and there are two you need to know.

The access token is the credential the client presents to the resource server to get at protected data. It is a bearer token, meaning whoever holds it can use it, so it should be short-lived and scoped to the minimum access needed. Its usage is defined in RFC 6750.

The refresh token is a longer-lived credential the client uses to obtain new access tokens when the old ones expire, without dragging the user back through a login prompt. Because it can mint fresh access tokens, a refresh token is higher value and demands more careful storage and rotation.

The design intent is a short window of exposure for the token that flies around on every request, backed by a longer-lived token held more carefully. Short access token lifetimes limit the damage of a leak, because a stolen access token expires quickly.

Access tokens come in two broad shapes, and the difference affects how the resource server validates them. A reference token is an opaque string that means nothing on its own; the resource server checks it by asking the authorization server (or a shared store) whether it is valid and what it grants. A self-contained token, commonly a JWT, carries its claims inside itself, signed by the authorization server, so the resource server can validate it locally without a round trip. Self-contained tokens scale well but are harder to revoke before expiry, which is another reason to keep their lifetimes short.

Bearer tokens are like cash

An access token is a bearer credential: the resource server honours whoever presents it, and does not re-check that it is really you. That is why tokens must travel only over TLS, must never land in logs or URLs that get stored, and should be short-lived and narrowly scoped. Treat a leaked token as a leaked credential, because that is exactly what it is.

Grant types and when to use them

A grant type is the specific sequence a client follows to obtain a token. OAuth 2.0 defines several, and the right choice depends on the kind of client.

Authorization Code grant. The standard flow for an application acting on behalf of a user. The user authenticates at the authorization server, which returns a short-lived authorization code to the client, and the client exchanges that code for tokens over a back-channel request. Tokens never appear in the browser address bar. This is the recommended default.

Authorization Code with PKCE. PKCE (Proof Key for Code Exchange) extends the Authorization Code grant so it is safe for public clients that cannot hold a secret. The client generates a random secret per request and proves possession of it when redeeming the code, which stops an intercepted authorization code from being redeemed by an attacker. Current guidance is to use PKCE for essentially all Authorization Code flows, public or confidential. Microsoft documents this as the authorization code flow.

Client Credentials grant. For machine-to-machine access where no user is involved, such as one backend service calling another. The client authenticates as itself with its own credentials and receives a token scoped to its own permissions.

Device Authorization grant. For input-constrained devices such as smart TVs, where the user completes authorization on a separate device like a phone.

A quick way to reason about which grant fits: ask whether a user is present, and whether the client can keep a secret. If a user is present, use Authorization Code with PKCE regardless of whether the client is confidential or public. If no user is present and one service is calling another, use Client Credentials. If a user is present but the device cannot show a proper browser, use Device Authorization. Those three cover almost every legitimate case, and reaching for anything else is usually a sign the design has taken a wrong turn.

Two older grants should be avoided in new designs:

  • The Implicit grant returned tokens directly in the browser redirect. It exposed tokens in the URL and lacked the protections of the code flow. Current best practice replaces it with Authorization Code plus PKCE.
  • The Resource Owner Password Credentials grant had the client collect the user's actual username and password. That defeats the core purpose of OAuth and is discouraged.
Grant typeBest forStatus
Authorization Code + PKCEUser-facing web, mobile, and SPA clientsRecommended default
Client CredentialsMachine-to-machine, no userRecommended for its use case
Device AuthorizationInput-constrained devicesRecommended for its use case
Implicit(formerly SPAs)Discouraged, use Code + PKCE
Resource Owner Password(legacy migration only)Discouraged

Why token handling is a security control

Because tokens are bearer credentials, stealing one is a direct route to a user's data or an application's access. On MITRE ATT&CK this is T1528, Steal Application Access Token, and it shows up in real intrusions where attackers phish OAuth consent or lift tokens from a compromised host to reach cloud data without touching a password. Our threat group intelligence tracks which groups run OAuth consent phishing and token theft, so token handling can be prioritised against techniques in active use. The token is the target, so how you issue, scope, store, and expire tokens is the security posture.

Scope down and expire fast

The two most effective token controls are narrow scope and short lifetime. A token that can only read one resource type, and only for a few minutes, is worth far less to an attacker than a broad, long-lived one. Combine that with refresh token rotation, so a stolen refresh token becomes detectable when the legitimate client and the attacker both try to use it.

How to deploy OAuth 2.0 safely

  1. Use Authorization Code with PKCE as the default for any client acting for a user, and Client Credentials for machine-to-machine. Do not use the Implicit or Password grants in new work.
  2. Register exact redirect URIs and match them strictly, so an attacker cannot redirect the authorization response to a URL they control.
  3. Keep access tokens short-lived and narrowly scoped, requesting only the permissions the feature needs.
  4. Protect and rotate refresh tokens, storing them server-side where possible and enabling rotation so reuse is detectable.
  5. Send everything over TLS, and keep tokens out of URLs, logs, and anywhere they might be persisted or cached.
  6. Validate tokens at the resource server, checking signature, issuer, audience, expiry, and scope before serving data.
  7. Make consent legible, so users can see and revoke which applications hold access to their account.

A worked example: the Authorization Code flow with PKCE

Following one flow end to end makes the roles and tokens concrete. Take a mobile app that wants to read a user's calendar from a cloud provider.

The app is a public client, because anything shipped in a mobile binary can be extracted, so it uses PKCE. Before starting, it generates a random high-entropy string called the code verifier, then computes a SHA-256 hash of it called the code challenge. It keeps the verifier in memory and sends only the challenge in the first step.

The app opens the system browser to the authorization server's authorize endpoint. The request carries the client identifier, the requested scope such as calendar.read, the exact redirect URI registered for the app, a random state value to tie the response back to this request, and the code challenge. The app never handles the user's password. The authorization server shows its own login page, authenticates the user, and displays a consent screen naming the app and the scope it asked for.

When the user approves, the authorization server redirects the browser back to the app's registered redirect URI with two values: an authorization code and the state echoed back. The app checks that the returned state matches the one it sent, which defends against a forged response. The code itself is short-lived and single-use, and on its own it is not enough to get a token.

The app now makes a back-channel POST to the token endpoint. It sends the authorization code, the same redirect URI, its client identifier, and the code verifier it kept from the start. The authorization server hashes the verifier and compares it to the challenge it stored earlier. If they match, the server knows this is the same client that began the flow, and it returns an access token, a refresh token, and metadata such as the token lifetime and granted scope. The app then calls the calendar API, presenting the access token in the Authorization header as a bearer token, and the resource server serves the data.

The PKCE binding is what protects the public client. If an attacker intercepts the authorization code, perhaps through a malicious app that registered the same redirect scheme, they still cannot redeem it, because they do not hold the code verifier that produces the stored challenge. This is why current guidance applies PKCE to every Authorization Code flow.

What OAuth deliberately leaves out

OAuth 2.0 is a framework, which means it specifies the roles, the grants, and the token exchange, and it leaves several decisions to the implementer or to companion specifications. Knowing what it does not define prevents a class of design mistakes.

It does not define the format of an access token. A token can be an opaque reference or a signed JWT, and the choice is yours. It does not define how the resource server validates a token beyond the bearer usage rules, which is why token introspection and JWT validation are handled by separate specifications. It does not, by itself, tell the client anything about who the user is. The access token is meant for the resource server, and clients that read its contents to identify the user are making a well-known error that OpenID Connect exists to fix.

This gap between authorization and authentication is the single most consequential thing to internalize. OAuth grants an application access to a resource. Establishing the user's identity for the purpose of logging them in is the job of OpenID Connect, which adds an ID token, a signed statement about the authenticated user that is meant for the client to read.

ConcernHandled byToken involved
Can this app access this resourceOAuth 2.0Access token
Who is the user, for loginOpenID ConnectID token
Is this opaque token still validToken IntrospectionAccess token
Proving the client began the flowPKCECode verifier and challenge

Detection signals: spotting token abuse

Because tokens are the target, monitoring their use is where OAuth security becomes observable. Several signals separate normal activity from abuse.

A refresh token that is presented after it was already rotated is a strong signal of theft. With refresh token rotation, each use of a refresh token issues a new one and invalidates the old. If the old token is used again, either the legitimate client or an attacker is replaying a stolen copy, and the safe response is to revoke the whole token family and force re-authentication.

An access token used from an IP address, device, or geography that does not match where it was issued suggests the token has moved to another party. Bearer tokens carry no proof of who is holding them, so this contextual mismatch is often the only signal available. Sender-constrained tokens, using DPoP or mutual TLS, close this gap by binding a token to a key the client holds, so a stolen token alone is useless.

Consent grants are their own signal. A sudden spike in users granting a newly registered application broad scopes, especially scopes like full mailbox access, is the fingerprint of a consent phishing campaign. Reviewing which applications hold which scopes, and alerting on new high-privilege grants, catches this early.

At the resource server, tokens that fail audience or issuer checks, tokens presented long after issue for a short-lived design, and a rise in token validation failures all point to either misconfiguration or an attacker probing with tokens that were not minted for this API.

A leaked access token is used, not cracked

Because an access token is a bearer credential, an attacker who obtains one does not need to break any cryptography. They present it and the resource server honors it until it expires. This is why short lifetimes, narrow scopes, and sender-constrained tokens matter so much: they are the only things that limit the value of a token the moment it leaves the client's control.

Common misconceptions

Several beliefs about OAuth lead to insecure designs.

The first is that OAuth logs users in. It does not, on its own. It authorizes an application to access a resource. Treating the presence of a valid access token as proof of the user's identity leads to broken login systems. Use OpenID Connect and its ID token when the goal is authentication.

The second is that the Implicit grant is still the right choice for single-page apps. It is not. The Implicit grant returned tokens directly in the browser redirect, exposing them in the URL, and current guidance replaces it with the Authorization Code grant plus PKCE, which single-page apps can run entirely in the browser.

The third is that a JWT access token cannot be revoked, so revocation is impossible. The accurate statement is that a self-contained token is valid until it expires because the resource server validates it locally. You manage this by keeping lifetimes short and, where you need immediate revocation, by using introspection or a revocation check for sensitive operations.

The fourth is that scopes are a formality. Scopes are the mechanism that limits what a token can do. Requesting the narrowest scope a feature needs is a direct reduction in the damage a leaked token can cause.

The fifth is that registering a wildcard or loosely matched redirect URI is a convenience with no downside. It is a serious weakness. An authorization server that accepts a redirect URI it did not exactly register can be tricked into sending the authorization code or token to a URL the attacker controls. Register full, exact redirect URIs and match them string for string, so the authorization response can only ever land where the legitimate client expects it.

Frequently asked questions

What is the difference between OAuth 2.0 and OpenID Connect? OAuth 2.0 is an authorization framework that issues access tokens so an application can reach a resource. OpenID Connect is an identity layer on top of OAuth that adds an ID token, a signed statement about the authenticated user meant for the client to read. Use OAuth when an app needs to access an API on the user's behalf, and OpenID Connect when the goal is to log the user in.

Why is the Implicit grant discouraged now? It returned tokens in the browser redirect URL, where they could land in browser history, server logs, and referrer headers, and it lacked the code exchange step that PKCE protects. Browsers and libraries now support the Authorization Code grant with PKCE directly, giving single-page apps a safer flow with the same reach.

Where should a refresh token be stored? Server-side wherever possible, so it never sits in the browser. For public clients that must hold one, use secure platform storage, keep the lifetime bounded, and enable refresh token rotation so a stolen token is detectable when both the attacker and the legitimate client try to use it.

What does PKCE actually protect against? It protects against an intercepted authorization code being redeemed by an attacker. The client proves it began the flow by presenting a secret verifier that matches a challenge sent at the start. Without the verifier, a stolen code cannot be exchanged for tokens, which matters most for public clients that cannot hold a client secret.

Can I use one access token across several APIs? You can if the token's audience covers them, but it is usually better to scope tokens to a single resource server. A token minted for one API and accepted by another widens the blast radius of a leak. The audience claim exists so a resource server can reject tokens that were not issued for it.

How short should an access token live? Short enough that a leaked token expires before it is worth much, and long enough to avoid constant refresh traffic. Minutes to a small number of hours is common. Pair short access tokens with refresh tokens so the user is not re-prompted, and lean on rotation to detect refresh token theft.

Is a bearer token safe to send to any endpoint? No. Send it only to the intended resource server, over TLS, in the Authorization header. Sending a bearer token to the wrong host hands that host a working credential. Sender-constrained approaches such as DPoP or mutual TLS reduce this risk by binding the token to a key the client holds.

OAuth 2.0 solved a real problem cleanly: delegated access without password sharing. The framework is sound, and most weaknesses in the wild come from choosing the wrong grant or handling tokens carelessly. Pick the Authorization Code flow with PKCE, keep tokens short and scoped, guard the refresh tokens, and OAuth does exactly what it was designed to do.

Sources & further reading

Sharetwitterlinkedin

Related guides