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:

  1. 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
  1. The authorization server authenticates the user and shows a consent screen. The user approves (or denies) the requested scopes.

  2. The authorization server redirects back to your redirect_uri with an authorization code:

GET https://yourapp.com/callback?code=abc123&state=random_csrf_token
  1. 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

  1. 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.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Freelancers vs Agencies vs In-House Teams

“Should we hire freelancers, an agency, or build an in-house team?” The answer isn’t about which is best—it’s about what your situation actually needs.

Read more

A Good API Is One Developers Never Have to Ask Questions About

APIs fail when they require interpretation instead of execution. The best APIs eliminate ambiguity through consistent design, predictable behavior, and self-evident contracts.

Read more

Why Munich Pays $115/hr for Senior Backend Work — and How Remote Contractors Change That

You converted your backend engineer's fully loaded cost to an hourly rate. The number made you question every assumption in your financial model.

Read more

How San Francisco Founders Cut Backend Burn Rate by 60% Without Sacrificing Code Quality

Your backend team costs $80K a month fully loaded. The output is good. The question is whether you need $80K a month of permanent cost to get it.

Read more