The “Sign in with Google” button seems straightforward. Click it, authenticate, and you’re in. But behind that simple interaction lies one of the most widely deployed authorization protocols in computing history—a protocol that was never actually designed for authentication.
OAuth 2.0, published as RFC 6749 in October 2012, emerged from a practical problem: how do you let a third-party application access your data without giving it your password? The solution involved a clever dance of redirects, temporary credentials, and cryptographic proofs that billions of users perform daily without understanding what’s happening.
The Problem OAuth Was Built to Solve
In 2006, developers faced a dilemma. A photo-printing service wanted access to your photos stored on another platform. The obvious solution—asking for your username and password—was terrible security practice. It gave the printing service complete access to your entire account, not just photos. It created a single point of failure. And it meant changing your password broke every connected application.
OAuth introduced a different model: delegated authorization. Instead of sharing credentials, you grant specific permissions to specific applications for specific purposes. The printing service gets a token that allows it to read your photos—and nothing else. The token can be revoked without changing your password. And your actual credentials never leave the authorization server.
But here’s what makes OAuth fascinating: it was designed for authorization, not authentication. The fact that it’s now the dominant authentication mechanism on the web is an evolution that happened after the fact. OpenID Connect, built on top of OAuth, added the identity layer that turned an authorization protocol into an authentication standard.
The Four Roles
OAuth defines four distinct participants in every authorization flow:
Resource Owner: The user who owns the data and can grant access to it. In most cases, this is you.
Client: The application requesting access to protected resources. Note that “client” doesn’t mean a browser or mobile device—it means the application making the request, whether that’s a web server, a native app, or a single-page application.
Authorization Server: The server that authenticates the resource owner and issues tokens. This is the identity provider you trust with your credentials.
Resource Server: The server hosting the protected data. It accepts and responds to requests using access tokens.
In many implementations, the authorization server and resource server are the same entity, but they’re logically separate. An authorization server might issue tokens accepted by multiple resource servers.
Authorization Code Flow: The Redirect Dance
The authorization code flow is the most secure and most widely used OAuth grant type. It works through a series of HTTP redirects and server-to-server communications that keep sensitive tokens out of the browser.

Image source: Jacco Meijer - OAuth 2.0 and OpenID Connect Sequence Diagrams
Step 1: Authorization Request
The flow begins when the client redirects the user’s browser to the authorization server’s authorization endpoint. This isn’t a direct API call—it’s a browser redirect that includes several critical parameters:
GET /authorize?
response_type=code&
client_id=abc123&
redirect_uri=https://client.example.com/callback&
scope=openid profile email&
state=xyz123 HTTP/1.1
Host: auth.example.com
The response_type=code indicates this is an authorization code flow. The client_id identifies which application is making the request. The redirect_uri tells the authorization server where to send the user after authorization—this must match a pre-registered URI to prevent open redirect attacks. The scope parameter specifies what permissions are being requested. And the state parameter is a random string the client generates to protect against CSRF attacks.
Step 2: User Authentication and Consent
The authorization server presents a login screen to the user. Once authenticated, it shows a consent screen: “This application wants to access your email and profile. Allow?”
This consent step is crucial. OAuth doesn’t just authenticate the user—it explicitly asks the user to delegate specific permissions to the client application. The user can approve or deny each requested scope (though most implementations present an all-or-nothing choice).
Step 3: Authorization Code Response
If the user consents, the authorization server redirects back to the client’s redirect_uri with an authorization code:
HTTP/1.1 302 Found
Location: https://client.example.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA&
state=xyz123
The authorization code is a short-lived, single-use credential. It’s typically valid for only a few minutes and can only be exchanged once. The state parameter is returned exactly as it was sent—if it doesn’t match, the client must reject the response.
Step 4: Token Exchange
Here’s where the security magic happens. The client makes a direct server-to-server request to the authorization server’s token endpoint, exchanging the authorization code for actual tokens:
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://client.example.com/callback&
client_id=abc123&
client_secret=secret
This request happens over a direct HTTPS connection between the client’s server and the authorization server. The browser never sees the access token. For public clients (like single-page applications that can’t keep secrets), the client_secret is omitted and other security mechanisms come into play.
Step 5: Token Response
The authorization server validates the request and returns the tokens:
{
"access_token": "SlAV32hkKG",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBtZp8",
"scope": "openid profile email"
}
The access token is what the client uses to access protected resources. The refresh token allows the client to obtain new access tokens without user interaction. Access tokens typically expire in minutes or hours; refresh tokens can be valid for days or months.
PKCE: Closing the Code Interception Vulnerability
The authorization code flow has a weakness: what if an attacker intercepts the authorization code before the legitimate client can exchange it? This is particularly problematic for native mobile apps and single-page applications where the redirect URI might use a custom URL scheme that malicious apps can register.
PKCE (Proof Key for Code Exchange), defined in RFC 7636, solves this by having the client prove it’s the same entity that initiated the flow. Here’s how it works:
Before starting the flow, the client generates a cryptographically random code_verifier—a string between 43 and 128 characters. It then creates a code_challenge by hashing this verifier with SHA-256 and base64url-encoding the result:
code_verifier = random_string(43-128 chars)
code_challenge = BASE64URL(SHA256(code_verifier))
The authorization request includes the code_challenge:
GET /authorize?
response_type=code&
client_id=abc123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
When exchanging the code for tokens, the client sends the original code_verifier:
POST /token HTTP/1.1
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
The authorization server hashes the verifier and compares it to the stored challenge. If they match, the token is issued. An attacker who intercepts the authorization code won’t have the code_verifier, so they can’t complete the exchange.
Token Types: JWT vs Opaque
Access tokens come in two flavors, each with different trade-offs.
JWT (JSON Web Token) tokens are self-contained. They encode claims about the token itself—who issued it, who it’s for, when it expires, and what scopes it grants. A resource server can validate a JWT locally by checking the signature against the authorization server’s public key, without making a network call.
{
"iss": "https://auth.example.com",
"sub": "user123",
"aud": "https://api.example.com",
"exp": 1516239022,
"scope": "read write"
}
Opaque tokens are random strings that mean nothing on their own. To validate them, a resource server must make a call to the authorization server’s introspection endpoint. This adds latency but enables instant token revocation—the authorization server can simply mark the token as invalid.
The choice between them involves a fundamental trade-off: JWT tokens enable stateless, high-performance validation at the cost of harder revocation. Opaque tokens enable instant revocation at the cost of requiring network calls for every validation.
Security Vulnerabilities and Mitigations
OAuth’s flexibility creates opportunities for implementation mistakes. The OAuth 2.0 Threat Model (RFC 6819) documents dozens of potential attacks.
CSRF via Missing State Parameter
The state parameter isn’t optional—it’s critical for preventing cross-site request forgery. Without it, an attacker can initiate an OAuth flow with their own account, then trick a victim into completing it. The client receives a valid authorization code, exchanges it for tokens, and logs the victim into the attacker’s account.
Open Redirect via Redirect URI Manipulation
If an authorization server doesn’t strictly validate the redirect_uri, attackers can redirect authorization codes or tokens to attacker-controlled endpoints. Best practice is to require exact matching against pre-registered URIs—no wildcards, no path prefix matching.
Authorization Code Injection
An attacker who obtains an authorization code (through a compromised redirect or logging) can attempt to exchange it before the legitimate client. PKCE prevents this for public clients. For confidential clients, proper client authentication is essential.
Scope Upgrade Attacks
Some flawed implementations allow clients to request additional scopes during the token exchange that weren’t approved during authorization. The token endpoint must validate that requested scopes match those authorized by the user.
OAuth 2.1: Learning from a Decade of Mistakes
OAuth 2.1, currently in draft, consolidates security best practices learned from years of production deployments. The most significant changes:
PKCE is mandatory for all clients. The specification requires PKCE for both public and confidential clients, recognizing that code interception attacks aren’t limited to mobile apps.
Implicit grant is removed. The implicit flow, which returned access tokens directly through the browser URL fragment, has been deprecated. Modern single-page applications should use authorization code flow with PKCE instead.
Refresh token rotation is required for public clients. When a refresh token is used, a new one is issued and the old one is invalidated. This limits the damage from refresh token theft.
Strict redirect URI matching. The specification requires exact matching of redirect URIs, eliminating the flexible matching that has caused so many vulnerabilities.
References
- RFC 6749: The OAuth 2.0 Authorization Framework - https://datatracker.ietf.org/doc/html/rfc6749
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clients - https://datatracker.ietf.org/doc/html/rfc7636
- RFC 6819: OAuth 2.0 Threat Model and Security Considerations - https://datatracker.ietf.org/doc/html/rfc6819
- OAuth 2.0 Security Best Current Practice - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
- OAuth 2.1 Draft Specification - https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11
- PortSwigger Web Security Academy: OAuth 2.0 authentication vulnerabilities - https://portswigger.net/web-security/oauth
- Microsoft Identity Platform: OAuth 2.0 authorization code flow - https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow