DEV Community

Cover image for Understanding OAuth2 the Simple Way
G Ghuman
G Ghuman

Posted on

Understanding OAuth2 the Simple Way

Understanding OAuth2 the Simple Way

You are building a side project and want users to log in with their Google accounts instead of making up yet another password they'll forget. So you open the OAuth2 spec. Ten minutes later, you are staring blankly at a wall of terminology about "Resource Owners," "Authorization Grants," and "Bearer Tokens," wondering if you should just use a plaintext database table and call it a day.

Every tutorial on the internet tries to explain OAuth2 with an analogy about valet keys at a restaurant. I don't want a valet key. I want to know why my redirect URI is returning a 400 Bad Request and how to get an access token without losing my sanity.

Let's skip the car analogies and look at how this actually works.

The Core Concept: Delegation, Not Authentication

Here is the first trap everyone falls into: treating OAuth2 as a login system.

It isn't one. OAuth2 is an authorization framework. It is designed to answer one question: Can application X access resource Y on behalf of user Z?

Authentication answers "Who are you?" (That's OpenID Connect, which is built on top of OAuth2, but we'll ignore that for a second). OAuth2 answers "What are you allowed to do?"

Imagine you built a little CLI tool that automatically backs up your GitHub repositories to a local drive. Your script needs access to your private repos, but you definitely don't want to hardcode your GitHub password into a Python file.

Instead, you want to go to GitHub, log in yourself, look at a scary warning screen that says "This app wants read access to your repositories," and click "Authorize." GitHub then hands your script a temporary backstage pass (an access token). Your script uses that pass to grab your code.

That handshake is OAuth2.

The Authorization Code Flow (The One You'll Actually Use)

There are several "grant types" in the OAuth2 spec. Ignore all of them except the Authorization Code Flow.

If you are building a traditional web app with a backend server, this is the flow you need. It keeps your app's secret key safe on the server where users can't see it in their browser dev tools.

Here is how the dance goes step by step:

  1. The Redirect: Your app sends the user to the identity provider (like Google or GitHub) with a URL that says, "Hey, send this user back to /callback when they log in, and tell them I need read:user permissions."
  2. The Consent: The user logs into Google, sees the consent screen, and clicks "Allow."
  3. The Code: Google redirects the user back to your app's /callback route with a temporary code stuck in the query string. This code is not the access token. It's a single-use receipt.
  4. The Exchange: Your backend takes that receipt, walks up to Google's token endpoint behind the scenes (along with your client secret), and trades it for the actual Access Token.

Let's look at what that token exchange looks like in practice. Here is a quick Node.js snippet using axios hitting a mock provider:

const axios = require('axios');

async function exchangeCodeForToken(authCode) {
  try {
    const response = await axios.post('https://oauth.example.com/oauth/token', {
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET,
      code: authCode,
      grant_type: 'authorization_code',
      redirect_uri: 'http://localhost:3000/callback'
    }, {
      headers: { 'Content-Type': 'application/json' }
    });

    // This is what you actually care about
    const { access_token, refresh_token, expires_in } = response.data;

    return access_token;
  } catch (error) {
    // Spoiler: You will hit this catch block a lot at first
    console.error('Token exchange failed:', error.response.data);
    throw new Error('OAuth dance failed');
  }
}
Enter fullscreen mode Exit fullscreen mode

Making the Authenticated Request

Once you have that access_token safely stored in a secure, HTTP-only cookie or a session, using it is anti-climactic. You just slap it into the HTTP headers of your API requests.

import requests

def get_user_profile(access_token):
    url = "https://api.example.com/v1/user/profile"

    headers = {
        "Authorization": f"Bearer {access_token}",
        "Accept": "application/json"
    }

    response = requests.get(url, headers=headers)

    if response.status_code == 401:
        print("Token expired or invalid. Time to use that refresh token.")
        return None

    return response.json()
Enter fullscreen mode Exit fullscreen mode

That Bearer prefix trips people up. It literally means "Hand this token to whoever is bearing it." If someone steals your access token, they can impersonate the user until it expires. That is why keeping tokens out of localStorage in single-page apps matters so much—any rogue XSS script can read localStorage and steal your bearer tokens.

The Gotchas That Will Waste Your Afternoon

When I first wired this up, I spent three hours debugging a redirect_uri_mismatch error from Google. Everything looked identical. The string in my code matched the string in the Google Cloud Console down to the last slash.

Except it didn't. I had http://localhost:3000/callback in my code, but http://localhost:3000/callback/ (with a trailing slash) registered in the dashboard. OAuth implementations are aggressively literal. If a single character is off, they lock the door and give you zero helpful context.

Another common pitfall is ignoring token expiration. Access tokens are designed to be short-lived—sometimes expiring in 15 minutes. If your app crashes because a user's token expired while they were filling out a form, your UX is broken. You have to implement the refresh token cycle, which means storing that second token securely and asking the auth server for a fresh access token behind the scenes when the first one dies.

Finally, don't write your own OAuth2 server unless you are doing it purely for fun on a weekend. Use Auth0, Keycloak, Supabase, or Firebase Auth. Implementing the spec securely—handling state parameters to prevent CSRF, managing PKCE for mobile apps, handling token rotation—is tedious security plumbing that has already been solved a thousand times.

Next Steps

Open up your terminal, pick an API you use daily (GitHub or Spotify have great, developer-friendly docs), and register a new developer application in their dashboard. Don't write any code yet—just use Postman or curl to manually trigger the authorization URL in your browser, grab the code from the redirect, and paste it into a manual POST request to get your first token.

Once you trace the round-trip manually with your own eyes, the magic trick loses its mystery.

Top comments (0)