OAuth 2.0 flows explained: which one, and why the others are wrong
Most OAuth confusion is choosing a flow for the wrong reason. What each grant is for, why two of them are deprecated, and where real integrations actually break.
On this page
- What we'll cover
- What OAuth is and is not
- The four parties
- Authorization code with PKCE
- Client credentials, for machines
- The flows not to use
- Refresh tokens, where integrations break
- Scopes and consent
- What to do before you write any code
- Connecting with CodeWords
- What to get right first
- Frequently asked questions
- Related reading
OAuth 2.0 exists for one purpose: letting an application act on a person's data in another system without that person handing over their password. Everything in the specification follows from that, and most of the confusion comes from treating it as a general authentication framework, which it is not.
The practical version is shorter than the specification suggests. There is one flow you should use for user-facing applications, one for machine-to-machine calls, and several that exist for historical reasons and are now discouraged.
What we'll cover
What OAuth is and is not
It is delegated authorisation. A user grants your application limited access to their data somewhere else.
It is not authentication. An access token says an application may do something; it says nothing reliable about who the user is. Using one as proof of identity is a genuine vulnerability rather than a stylistic mistake.
OpenID Connect is the authentication layer built on top of it, adding an identity token with claims about the user. If you want "sign in with", that is what you want.
It is not a login system for your own application, unless you are using it to accept identity from an external provider.
The distinction matters practically: an access token can be obtained for a different application and presented to yours, which is why token-as-identity fails.
The four parties
Worth naming, because the specification's vocabulary is where people get lost.
The resource owner is the person whose data it is.
The client is your application, asking for access.
The authorization server issues tokens after the person consents.
The resource server holds the data and accepts the token.
The authorization server and resource server are frequently the same product and are separate roles, which is why the documentation distinguishes them.
Authorization code with PKCE
The flow to use for anything user-facing, and the answer in the large majority of cases.
The user is redirected to the provider, sees what is being requested, and approves.
The provider redirects back with a short-lived code, not a token.
Your application exchanges the code for tokens in a direct server-to-server call, so the token never passes through the browser's address bar.
PKCE binds the exchange to the original request. The client generates a secret, sends a hash of it when starting, and presents the original when exchanging — so an intercepted code cannot be redeemed by anybody else.
PKCE is now recommended for all clients, not only mobile and single-page applications, which is a change from older guidance still circulating.
Validate the state parameter on return, which is what prevents a cross-site request forgery against the callback.
Client credentials, for machines
Your application authenticates as itself, with no user involved.
Use it for service-to-service calls where the data belongs to your organisation rather than to a person.
It is a machine credential with a token exchange in front of it — the security benefit over a long-lived API key is that the resulting token is short-lived, which limits the value of interception.
No refresh token. You simply request another when the current one expires, which removes an entire category of the bugs described below.
Scope it narrowly, since a client credentials token frequently carries broad organisational access by default.
The flows not to use
Worth naming explicitly, since older tutorials still teach them.
Implicit flow returned tokens directly in the browser redirect, exposing them in the address bar and in history. It is discouraged in favour of authorization code with PKCE.
Resource owner password credentials has the user give your application their password, which defeats the purpose of OAuth entirely. It survives in legacy integrations and should not be chosen for anything new.
Device flow is not deprecated and is for a specific case: input-constrained devices where the user authorises on a second device. Use it when that is your situation.
If a provider only offers a deprecated flow, that is a fact about their API rather than permission — and worth raising with them, because these are being removed.
Refresh tokens, where integrations break
Almost every "the integration stopped working after a few weeks" report is here.
The access token is short-lived; the refresh token is the long-lived secret and the thing that actually needs protecting.
Refresh tokens frequently rotate. Each use issues a new one and invalidates the old, so the new value must be persisted immediately — storing it in configuration the process cannot write to is the classic error.
They expire if unused. A monthly job whose refresh token has an inactivity limit will break after a quiet period, and nothing warns you.
Concurrent refreshes invalidate each other. Two instances refreshing simultaneously means one wins and the other's token is dead, producing an intermittent failure that is hard to reproduce.
Consent can be revoked by the user or an administrator at any time, which looks identical to an expired token and needs a different fix — reconnecting rather than retrying.
Alert on refresh failure specifically, since the remedy usually involves a person re-authorising and nobody will know to do that from a generic error.
Scopes and consent
Request the minimum. A consent screen asking for broad access reduces the proportion of users who complete it, and a compromised token with broad scope is a larger incident.
Request incrementally where the provider supports it, asking for additional scopes when the feature that needs them is first used rather than all at once at sign-up.
Scopes are coarse. They limit categories of access, not individual records, so a scope is not a substitute for checking that this user may touch this object.
Explain the request. A short sentence in your own interface before the redirect, saying why you need what you are asking for, measurably helps.
Handle partial grants. Some providers let a user approve some scopes and decline others, and an application that assumes all-or-nothing will fail confusingly.
Re-check on change. Adding a scope to your application does not grant it for users who already authorised — they must consent again.
What to do before you write any code
Half the friction in an OAuth integration is configuration, and it is worth clearing before building.
Register the application and record the client identifier and secret somewhere the deployment can read.
Register every redirect URI, exactly — most providers match them strictly, and a trailing slash or a missing port is the difference between working and a generic error.
Register the local development URI too, or nobody can develop against it.
Check whether the provider requires review for the scopes you want. Sensitive scopes frequently need an application review with its own lead time, which is often longer than the build.
Find out whether sandbox and production are separate, with separate credentials and separate consent, because they usually are.
Read the provider's token lifetimes, including whether refresh tokens rotate and whether they expire when unused. Those two facts determine most of your error handling.
Connecting with CodeWords
CodeWords connects to more than 3,000 integrations, and the authorisation, token storage, and refresh handling are part of the connection rather than something to implement per automation.
- Open CodeWords and start a new automation.
- Describe what should happen in plain language to Cody, the automation builder.
- Authorize the connection to each system, granting the narrowest access the workflow needs.
- Describe the exceptions: an expired credential, a revoked authorisation, a permission the automation lacks.
You describe the outcome; Cody builds it, connects it, and deploys it. The free plan covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.
What to get right first
Refresh token handling: stored somewhere writable, updated immediately when rotated, with only one refresh happening at a time. It is where essentially every OAuth integration eventually fails, and the failure appears weeks after everything worked perfectly in testing.
Two habits make the difference. Exercise the refresh path deliberately during development rather than waiting for the access token to expire naturally, since a flow that is never tested is a flow that is never correct. And alert distinctly on a revoked authorisation, because that one needs a person to reconnect and no amount of retrying will fix it.
Frequently asked questions
Which OAuth flow should I use?
Authorization code with PKCE for anything user-facing, and client credentials for service-to-service calls where no user is involved. Those two cover nearly every real case.
Can I use an access token to identify the user?
No. An access token says an application may do something, not who anybody is, and a token issued for a different application can be presented to yours. Use OpenID Connect, which adds an identity token for exactly this.
Why does my integration stop working after a few weeks?
Refresh handling. Refresh tokens frequently rotate and the new value must be stored immediately, in somewhere the process can write to. They also expire if unused, which breaks monthly jobs after a quiet period.
What is PKCE and do I need it?
It binds the token exchange to the original authorisation request, so an intercepted code cannot be redeemed by anybody else. Current guidance recommends it for all clients, not only mobile and browser-based ones.
Why does the connection fail intermittently?
Often two instances refreshing at once: one succeeds and invalidates the other's token. Serialise refreshes, or tolerate the failure by re-reading the stored token before giving up.
Should I request all scopes at sign-up?
No. Request the minimum, and ask for more when the feature that needs them is first used. Broad consent screens reduce completion, and adding a scope later requires users to consent again regardless.
The provider only offers the password flow — what now?
That is a fact about their API rather than a recommendation. Use it if you must, isolate it carefully, and raise it with them — these flows are being removed, and finding out during a deprecation is worse than asking now.
Why does the redirect fail with a vague error?
Almost always a redirect URI that does not match what is registered — providers compare exactly, so a trailing slash, a different port, or http against https all fail. The error is deliberately unhelpful for security reasons.
What should I check before building?
Registered redirect URIs including your local one, whether your scopes need provider review, whether sandbox and production are separate, and the token lifetimes — particularly whether refresh tokens rotate and whether they expire when unused.
How do I tell an expired token from a revoked authorisation?
The responses usually differ, and the distinction matters because only one is fixable by retrying. A revoked authorisation needs a person to reconnect, so it deserves its own alert rather than being folded into generic authentication failures.