The problem OAuth solves
Before OAuth, the way to let an app read your mailbox was to give it your email password. The app then had everything: it could read, send, delete and change the password itself, and the only way to take access back was to change the password and break every other app you had connected.
OAuth replaces the password with a token that is issued for one app, limited to named permissions, short-lived, and revocable on its own. The technical term is delegated authorization: you delegate part of your access to a program. Note that the word is authorization, not authentication. OAuth answers “what may this app do?”, not “who is this person?”; a separate layer, OpenID Connect, adds the second question.
The four roles
| Role | What it is | In the calendar example |
|---|---|---|
| Resource owner | The user who owns the data | You |
| Client | The application that wants access | The calendar app |
| Authorization server | Signs the user in, asks for consent, issues tokens | Google's account service |
| Resource server | The API that holds the data and accepts tokens | The Google Calendar API |
The client is registered with the authorization server in advance and receives a client ID, and, if it runs on a server that can keep one, a client secret. A browser or mobile app cannot keep a secret from its own user, so it registers as a public client and has none.
The authorization code flow
This is the flow for anything with a user in front of it, and the one to learn first.
1. The app sends the browser to the authorization server.
GET https://auth.example.com/authorize
?response_type=code
&client_id=calendar-app
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=calendar.read
&state=k3Vx9pQ2
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
2. The user signs in and approves. This happens on the authorization server's own page. The app never sees the password.
3. The browser is sent back to the app with a one-time code.
https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=k3Vx9pQ2
4. The app exchanges the code for tokens, in a direct request from its back end that the browser never sees.
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&client_id=calendar-app
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
→ 200 OK
{ "access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "calendar.read" }
5. The app calls the API with the access token.
GET /v1/events HTTP/1.1 Host: api.example.com Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
Why there are two steps, and what PKCE adds
The code travels through the browser's address bar, where history, extensions and logs can see it. So the code is made worthless on its own: it works once, expires in about a minute, and must be exchanged in a second request that proves the caller is the app that started the flow.
That proof is PKCE, pronounced “pixy”. The app invents a random
code_verifier, sends only its SHA-256 hash as the code_challenge in step 1,
and reveals the verifier itself in step 4. Someone who intercepts the code does not have the verifier
and cannot redeem it. PKCE was designed for mobile apps and is now recommended for every client,
including those that also hold a client secret. The state value is a separate protection:
the app checks that the value coming back matches the one it sent, which stops an attacker from
feeding it somebody else's code.
The other flows
| Flow (grant type) | Use it when | User involved |
|---|---|---|
| Authorization code + PKCE | A web, mobile or desktop app acts for a user | Yes |
| Client credentials | A service calls an API as itself: cron jobs, back-end to back-end | No |
| Device authorization | The device has no browser or keyboard: TVs, command-line tools | Yes, on a second device |
| Refresh token | Getting a new access token without asking the user again | No |
| Implicit | Do not use. Tokens were returned in the URL. | — |
| Resource owner password | Do not use. The app collects the user's password. | — |
The last two are in the original specification and are advised against by the current security best practice, RFC 9700. You will still find them in older integrations.
Client credentials is the simplest flow and the one most worth trying by hand, because it is a single request with no browser involved:
POST /token HTTP/1.1 Host: auth.example.com Authorization: Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ= Content-Type: application/x-www-form-urlencoded grant_type=client_credentials&scope=reports.read
The Authorization header is the client ID and secret joined by a colon and
Base64-encoded, which is ordinary HTTP Basic authentication. Note the
body: token requests are form-encoded, not JSON, and sending JSON is one of the commonest reasons a
first attempt fails.
Access tokens, refresh tokens and scopes
An access token is what the API accepts. It is sent as a
bearer token in the Authorization header, and it is
deliberately short-lived — often an hour — so that a stolen one stops working soon. OAuth does not say
what a token looks like. Some are random strings the API looks up; others are JWTs the API verifies by
signature, which you can inspect in the JWT decoder.
OAuth vs JWT explains how the two relate.
A refresh token is longer-lived and is sent only to the authorization server, in exchange for a fresh access token when the old one expires. That is how an app stays connected for months while every token the API sees is at most an hour old.
Scopes are the permissions: space-separated strings such as
calendar.read that the app requests in step 1 and the user sees on the consent screen. The
token carries only the scopes that were granted. Ask for the narrowest set that does the job; users
refuse broad requests, and a leaked narrow token does less harm.
OAuth is not login
An access token proves that someone authorised the app to call an API. It does not tell the app
who that person is, and using it as proof of identity has caused real account-takeover bugs.
OpenID Connect is a thin layer over OAuth that fills the gap: add the scope
openid, and the token response also contains an id_token, a signed JWT that
states who signed in, for which app, and when. “Sign in with Google” is OpenID Connect; “let this app
read my calendar” is plain OAuth.
And when no user is involved and the provider simply wants to identify your application, OAuth may be more than you need: an API key does that job. The API authentication guide sets the options side by side.
Errors you will meet
| Where | Error | Usual cause |
|---|---|---|
| Authorization page | redirect_uri mismatch | The URI sent is not character-for-character one of those registered for the client |
| Token endpoint, 400 | invalid_grant | The code was already used or has expired, the verifier does not match, or the refresh token was revoked |
| Token endpoint, 401 | invalid_client | Wrong client ID or secret, or sent in the body when the server expects Basic auth |
| Token endpoint, 400 | unsupported_grant_type | A JSON body instead of form encoding, or a flow not enabled for this client |
| API, 401 | invalid_token | The access token has expired or was issued for a different API |
| API, 403 | insufficient_scope | The token is valid but lacks the scope this endpoint needs |
On a 401 from the API, read the WWW-Authenticate response header; it names
the error and often the reason.
Trying a token request by hand
In the API tester, set the method to POST and the URL to the provider's
token endpoint, choose Basic auth in the Auth tab and enter the client ID and secret,
add the header Content-Type: application/x-www-form-urlencoded, and put
grant_type=client_credentials in the body. Copy the access_token from the
response, switch the Auth tab to Bearer token, and call the API with it. The request
is relayed through this site's server, so use a test client rather than production credentials; the
curl basic auth example gives the same request for your own terminal.
References
- RFC 6749: The OAuth 2.0 Authorization Framework — The core specification: roles, grant types and the token endpoint.
- RFC 6750: Bearer Token Usage — How an access token is sent to an API, and the invalid_token and insufficient_scope errors.
- RFC 7636: Proof Key for Code Exchange — PKCE: the code verifier and code challenge.
- RFC 9700: Best Current Practice for OAuth 2.0 Security — Current security advice, including which flows to stop using.
- OpenID Connect Core 1.0 — The identity layer built on OAuth 2.0, and the ID token.
These links leave api-tester.com, which is independent of the organisations they point to.
Frequently asked questions
What is OAuth in simple terms?
A way to give an application limited access to your account on another service without telling it your password. You approve the request on the service's own page, and the application receives a token that works only for what you approved.
Is OAuth authentication or authorization?
Authorization. It decides what an application may do, not who the user is. OpenID Connect, which is built on top of OAuth 2.0, adds authentication by issuing an ID token that identifies the user.
What is the difference between OAuth and an API key?
An API key identifies an application and usually lasts until revoked. An OAuth access token represents permission a specific user granted to an application, is limited to named scopes, and expires after a short time.
What is an access token?
The credential an OAuth client sends to an API, normally in an Authorization header after the word Bearer. It is short-lived and limited to the scopes that were granted. Its format is up to the provider and may be a random string or a JWT.
What is PKCE and do I need it?
Proof Key for Code Exchange: the client sends a hash of a random value when it starts the flow and the value itself when it redeems the code, so an intercepted code cannot be used by anyone else. Current guidance recommends it for every client using the authorization code flow.
Which OAuth flow should I use?
Authorization code with PKCE when a user is present, client credentials when a service calls an API as itself, and the device flow for hardware without a browser. The implicit and password flows are no longer recommended.