⏱️ Reading time: 13 min
Every time you tap “Continue with Google” on a new app and never type a password, you’re using OAuth 2.0. The protocol was formalized in RFC 6749 in October 2012, and today it underpins nearly every delegated login on the web.
📑 En este artículo
- TL;DR
- What is OAuth 2.0?
- Why the open authorization standard matters
- How the delegated authorization flow works
- Practical examples: how to get started with OAuth2
- Real-world use cases
- Common mistakes and best practices
- Comparison: OAuth2 versus other authorization standards
- Going deeper: PKCE and the inner workings of the OAuth2 flow
- Frequently Asked Questions
- Does OAuth 2.0 completely replace passwords?
- What’s the difference between OAuth2 and OpenID Connect?
- Is the implicit flow of the open authorization protocol still safe?
- What is PKCE and why does the delegated authorization flow need it?
- How long does an access token last in OAuth2?
- What happens if an OAuth2 refresh token leaks?
- References
Understanding it thoroughly avoids two common mistakes among developers: treating it as a login system and copying examples of already deprecated flows. This article explains its roles, its current flows, and the errors that most often break real implementations.
TL;DR
- OAuth 2.0 separates identity from authorization: an app gets access without ever seeing your password
- The authorization code flow with PKCE is the current standard for web and mobile apps
- Access tokens expire in minutes; refresh tokens renew them without asking for login again
- Confusing OAuth with an authentication protocol is the most common mistake when implementing it
- RFC 6749 defines four roles: resource owner, client, authorization server, and resource server
What is OAuth 2.0?
OAuth 2.0 is an open authorization protocol that lets an application access a user’s data on another service without ever handling or seeing their password. It works with temporary tokens issued by an authorization server, defined in the IETF’s RFC 6749 since 2012.
The protocol distinguishes four participants: the user who owns the data, the application that wants it, the server that issues the tokens, and the server that accepts them to deliver the resource. Nearly every public API today (GitHub, Spotify, Slack, Google Cloud) implements some variant of this scheme so a third party can gain access without asking for the original password.
Contrary to what many people assume, the authorization protocol does not verify identity on its own. That confusion is so common that it ended up spawning a separate standard, OpenID Connect, built precisely on top of OAuth to solve the “who is this user” problem that OAuth never promised to solve.
Why the open authorization standard matters
Before this standard, the only way for a third-party application to use your account on another service was to ask for your password directly. That pattern forced the user to trust their full credential to an app that could store it, leak it, or abuse it with no limit on scope.
The authorization protocol solves that problem by separating two things that used to go together: who you are and what an application can do on your behalf. A third-party app receives a token with limited permissions, never the password, and that token can be revoked at any time without having to change the access credential.
That separation enabled the integration ecosystem we take for granted today. When you connect Google Calendar to a productivity app, or authorize a Slack bot to read a channel, the user decides exactly what scope they grant (read-only, a single calendar, no access to contacts), and the application never stores the real password.
It also solved a scaling problem for large platforms. With short-lived, revocable tokens, a provider like GitHub can offer thousands of third-party OAuth Apps without a breach in one of them compromising the user’s full account across other integrations.
How the delegated authorization flow works
The standard defines four participants that interact in every exchange. The resource owner is the user who owns the data. The client is the application that wants to access that data on their behalf. The authorization server verifies the user’s identity and issues the tokens. The resource server is the API that ultimately delivers the data, validating the token before responding.
The following figure summarizes how these four roles relate to each other:
flowchart TD
U["User (resource owner)"] --> AS["Authorization server"]
AS -->|"issues code"| C["Client application"]
C -->|"exchanges code"| AS
AS -->|"issues access token"| C
C -->|"uses token"| RS["Resource server (API)"]
RS -->|"returns data"| C
RFC 6749 defines several “grant types,” each designed for a different kind of application. The most widely used today is the authorization code grant, designed for applications that can keep a secret on the server, or that use PKCE if they can’t. The client credentials grant is for server-to-server communication with no user involved. The device authorization grant, defined later in RFC 8628, solves the case of a smart TV or a console without a comfortable keyboard: the device displays a short code and the user enters it from their phone.
⚠️ Heads up: the implicit flow (response_type=token) returned the access token directly in the browser URL. It ended up exposed in browser history and server logs, so the current revision of the standard discourages it for new projects in favor of the authorization code grant with PKCE.
In the code flow, the sequence is always the same. The application redirects the user to the authorization server with the list of requested scopes. The user approves or denies access from a screen controlled by the provider itself, never by the client application. If they approve, the server redirects back with a single-use authorization code, which the application exchanges for an access token by calling the server directly, without the browser taking part in that step.
sequenceDiagram
participant U as User
participant C as Application
participant AS as Authorization server
participant RS as Resource server
C->>U: redirects to login screen with code_challenge
U->>AS: approves access
AS-->>C: redirects with authorization code
C->>AS: exchanges code + code_verifier for token
AS-->>C: responds with access token
C->>RS: requests data with the token
RS-->>C: responds with the resource
Note over C,AS: the code_verifier prevents an attacker from using an intercepted code
The resulting access token is usually opaque or in JWT format, and it lives for a short time: minutes or hours. Along with it, the server usually issues a longer-lived refresh token, which the application exchanges for a new access token without the user having to approve anything again, until that refresh token also expires or is revoked.
Practical examples: how to get started with OAuth2
To see the full flow in action, all you need is a browser, curl, and a GitHub account. GitHub exposes a standard authorization code flow that serves as a real example of how any OAuth2 integration works in production.
Before writing a single command, you need to register an OAuth App at github.com/settings/developers. That registration gives you a public client_id and a private client_secret, and asks you to declare a redirect_uri (for this example, http://localhost:3000/callback).
Step 1: build the authorization URL
https://github.com/login/oauth/authorize?client_id=Iv1.8a61f9b3a7aba766&redirect_uri=http://localhost:3000/callback&scope=read:user
When you open this URL in the browser, GitHub shows the approval screen. If the user accepts, GitHub redirects to http://localhost:3000/callback?code=1a2b3c4d5e6f7g8h: that code is the single-use authorization code.
Step 2: exchange the code for a token
curl -X POST https://github.com/login/oauth/access_token \
-H "Accept: application/json" \
-d client_id=Iv1.8a61f9b3a7aba766 \
-d client_secret=your_client_secret \
-d code=1a2b3c4d5e6f7g8h
Expected output:
{"access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a","token_type":"bearer","scope":"read:user"}
Step 3: call the API with the token
curl -H "Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a" https://api.github.com/user
Expected output (simplified):
{"login":"octocat","id":1,"name":"The Octocat"}
Step 4: verify the granted scope
To confirm which permissions were actually authorized for that token, you need to look at the response headers, not just the body:
curl -I -H "Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a" https://api.github.com/user
The X-OAuth-Scopes: read:user header in the response confirms that the token has exactly the scope requested, no more and no less.
Real-world use cases
The most visible case for any user is the “Continue with Google” or “Continue with GitHub” button that appears on thousands of products. That button almost always combines the authorization protocol with OpenID Connect: the first gives the application access to specific data (email, profile), the second confirms the identity of whoever logged in.
Third-party integrations on social networks are another everyday example. An app that schedules posts on X, or a bot that reads messages in a Slack channel, receives a token with specific scopes (post, read a channel) and never sees the account’s password.
In payments, Stripe Connect uses a variant of the same scheme so a marketplace platform receives authorization from each seller to process their charges, without the seller sharing their banking credentials with the marketplace. On devices without a comfortable keyboard, like a smart TV, services such as YouTube use the device authorization grant: the TV displays a short code, the user types it in from their phone, and the token ends up reaching the device without it ever having had direct access to the login form.
Common mistakes and best practices
- Confusing authorization with authentication: using the access token to decide “who” the user is leaves the developer treating a token as proof of identity, something the standard never guaranteed.
- Omitting the state parameter: without a random value verified when returning from the authorization server, the application is exposed to CSRF attacks that force the user to authorize an account that isn’t theirs.
- Not implementing PKCE in public clients: an SPA or mobile app can’t keep a real client_secret, so without code_verifier and code_challenge, an intercepted authorization code is enough to steal the entire token.
- Storing tokens in localStorage: a token accessible from JavaScript is exposed to any XSS on the page; httpOnly and secure cookies reduce that attack surface.
- Requesting excessive scopes: asking for full access when the application only needs to read one specific piece of data makes the damage worse in the event of a breach, and also lowers the users’ approval rate.
💡 Tip: if your application is an SPA or a mobile app, always use PKCE. Without it, an intercepted authorization code is enough to steal the access token without needing the client_secret.
Comparison: OAuth2 versus other authorization standards
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| OAuth 2.0 (code + PKCE) | Web or mobile apps that access third-party APIs | Short-lived, revocable tokens, granular scopes | Doesn’t authenticate identity on its own |
| OpenID Connect | Login or “Continue with X” | Adds an identity layer (ID token) on top of OAuth2 | Depends on the provider’s OAuth2 implementation |
| API Keys | Internal APIs or simple server-to-server integrations | Simplicity, no redirect flow | No expiration or granular scopes; full access if leaked |
| SAML | Traditional enterprise SSO | Mature and standard in corporate environments | Heavy for REST APIs or modern mobile apps |
Going deeper: PKCE and the inner workings of the OAuth2 flow
PKCE (Proof Key for Code Exchange, RFC 7636) solves a specific problem for public clients: an SPA or a mobile app can’t keep a client_secret without exposing it in the code. Without that secret, anyone who intercepts the authorization code (for example, via a misconfigured deep link) could exchange it for a token.
The solution is to generate, on the client itself, a random string called code_verifier. Before redirecting the user, the application computes code_challenge = BASE64URL(SHA256(code_verifier)) and sends it along with the authorization request. When it comes time to exchange the code for the token, the application sends the original code_verifier; the server recomputes the hash and compares it against the code_challenge it stored at the start. If they don’t match, it rejects the exchange, even if the attacker has the code.
Another detail that separates mature implementations from basic ones is whether the access token is opaque or a signed JWT. An opaque token forces the resource server to query the authorization server on every request via the introspection endpoint (RFC 7662). A JWT, on the other hand, allows the signature to be validated locally without that extra call, at the cost of not being able to revoke it instantly without a separate revocation list.
stateDiagram-v2
[*] --> Issued
Issued --> Active
Active --> Expired: the useful lifetime minutes pass
Expired --> Active: the refresh token is used
Active --> Revoked: the user withdraws access
Revoked --> [*]
Expired --> [*]: the refresh token also expired
A more recent development from the ecosystem itself is DPoP (RFC 9449), which binds the access token to a cryptographic key that only the legitimate client holds. That way, stealing the token alone (without that key) is no longer enough to use it elsewhere, something traditional bearer tokens could never prevent.
Your next step: register a free OAuth App at github.com/settings/developers and repeat the four curl commands from this article against your own account to watch the code-for-token exchange happen live.
Frequently Asked Questions
Does OAuth 2.0 completely replace passwords?
No. The user still authenticates with a password (or passkey, or 2FA) directly on the authorization server. What OAuth prevents is that password being shared with the third-party application.
What’s the difference between OAuth2 and OpenID Connect?
OAuth2 authorizes access to data or actions; OpenID Connect is built on top of it to confirm identity, adding a signed ID token that certifies who logged in.
Is the implicit flow of the open authorization protocol still safe?
It’s not recommended for new projects. It returns the token directly in the browser URL, which exposes it in history and logs; the authorization code grant with PKCE replaces it in practically every case.
What is PKCE and why does the delegated authorization flow need it?
It’s an extension (RFC 7636) that protects public clients, like SPAs and mobile apps, which can’t keep a client_secret without exposing it in the source code.
How long does an access token last in OAuth2?
It varies by provider, but it usually ranges from minutes to a few hours. The short lifespan is intentional: it limits the damage if the token leaks.
What happens if an OAuth2 refresh token leaks?
An attacker can generate new access tokens indefinitely until the user or the provider revokes it manually, which is why refresh tokens must be stored with at least as much care as a password.
References
- RFC 6749: the official IETF specification for OAuth 2.0.
- RFC 7636: defines PKCE, the extension for public clients.
- oauth.net/2: the OAuth community’s reference site, maintained by one of the standard’s co-authors.
- GitHub Docs: the official guide for implementing the authorization flow used in this article’s examples.
- RFC 8628: defines the device authorization grant used on TVs and consoles.
📱 Like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Featured image: Foto de Zaqy Al Fattah en Unsplash
Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.
Leave a comment
0 Comments