OAuth Is Confusing Until You Understand What Problem It Actually Solves
by Eric Hanson, Backend Developer at Clean Systems Consulting
The problem OAuth was designed to solve
Before OAuth, the common pattern for third-party integrations was simple and terrible: give the third party your username and password. A travel app that needed to read your email to find flight confirmations would ask for your Gmail credentials and store them. Every application that needed access to your account data had your full credentials.
The problems with this are obvious in retrospect: the third party could do anything with your account, not just the thing you authorized them for. You could not revoke their access without changing your password (breaking every other third-party that also had it). You had no visibility into what they were doing.
OAuth 2.0 (RFC 6749) solves this by introducing delegated authorization: you authorize a specific application to access specific resources on your behalf, without giving it your credentials. The authorization is scoped, revocable, and auditable.
Once you understand this, the flows stop looking like bureaucratic ceremony.
The actors and their roles
OAuth 2.0 defines four roles:
Resource Owner — the user who owns the data. When you authorize Notion to access your Google Calendar, you are the resource owner.
Client — the application requesting access. Notion is the client.
Authorization Server — the system that authenticates the resource owner and issues tokens. Google's OAuth server.
Resource Server — the API that holds the protected data. Google Calendar API. The resource server validates tokens and serves the data.
These are sometimes collapsed (the authorization server and resource server might be the same service) but keeping them conceptually separate clarifies why each step exists.
The Authorization Code flow step by step
This is the flow you implement for user-facing OAuth integrations:
- Your application redirects the user to the authorization server:
GET https://auth.provider.com/authorize
?response_type=code
&client_id=your_client_id
&redirect_uri=https://yourapp.com/callback
&scope=calendar:read
&state=random_csrf_token
-
The authorization server authenticates the user and shows a consent screen. The user approves (or denies) the requested scopes.
-
The authorization server redirects back to your
redirect_uriwith an authorization code:
GET https://yourapp.com/callback?code=abc123&state=random_csrf_token
- Your server exchanges the code for tokens — this exchange happens server-to-server, not in the browser:
POST https://auth.provider.com/token client_id=your_client_id client_secret=your_secret grant_type=authorization_code code=abc123 redirect_uri=https://yourapp.com/callback
- The authorization server returns an access token and a refresh token. Your application stores these and uses the access token for API calls.
Why the two-step (code → token exchange)? The authorization code travels through the browser (visible in redirects, history, server logs). The access token never does — it is exchanged server-to-server. If the authorization code is intercepted, it is useless without the client secret.
The state parameter is a CSRF defense: your application generates it, sends it to the authorization server, and verifies it matches on the callback. Without it, an attacker can redirect a user through a crafted authorization URL to link your user's account to the attacker's authorization code.
The grant types and when to use them
Authorization Code — user-facing integrations. Any time a human is authorizing access.
Authorization Code + PKCE (Proof Key for Code Exchange, RFC 7636) — mobile apps and SPAs that cannot safely store a client secret. PKCE replaces the client secret with a code verifier/challenge pair generated per-request. Required for public clients; use it by default.
Client Credentials — machine-to-machine. No user involvement. Service A authenticates directly to the authorization server with its client ID and secret to get a token for calling Service B. This is the right pattern for backend services calling other services.
Device Authorization Grant (RFC 8628) — devices without a browser (smart TVs, CLI tools). The device gets a user code, displays a URL, and polls for completion while the user authorizes on another device.
Do not use the Resource Owner Password Credentials grant (ROPC). It requires the client to handle the user's credentials, which is exactly what OAuth was designed to prevent. It exists for legacy migration and nothing else.
OIDC: what OAuth is not
OAuth 2.0 is an authorization framework. It does not define how to verify user identity — only how to get tokens that authorize access to resources. OpenID Connect (OIDC) is a thin identity layer on top of OAuth 2.0 that adds an ID token (a JWT with verified claims about the user's identity) to the token response.
If your OAuth integration is for user authentication — "sign in with Google" — you need OIDC, not bare OAuth 2.0. The access token alone does not tell you who the user is; the ID token does.
What to implement vs. what to delegate
Building your own OAuth authorization server is non-trivial. Unless you have specific requirements that off-the-shelf solutions cannot meet, use an existing OIDC-compliant provider. Keycloak (open source, self-hosted), Auth0, and Okta are the common options depending on your hosting and compliance requirements.
What you always implement yourself: your resource server validation logic (verifying tokens, checking scopes, enforcing authorization rules), your client registration flow if you accept third-party OAuth clients, and your consent UI if you are building a platform other applications integrate with.