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 involving "Resource Owners," "Authorization Servers," and "Grant Types," wondering if you actually need to learn cryptography just to let someone sign in with Google.

Most explanations of OAuth2 are written by security architects for other security architects. They read like legal contracts disguised as technical docs.

Let's skip the academic definitions. Here is how OAuth2 actually works when you're building a web app, without the enterprise fluff.

The Valet Key Analogy

Forget the RFC for a minute. Think about how you give your car to a valet.

You don't hand the valet the master key that opens your glove compartment, your trunk, and your house front door (which uses the same key, because your life is messy). You hand them a specific, restricted valet key that only starts the engine and moves the car forward and backward.

OAuth2 is just a digital valet key.

Your app wants to do something on behalf of a user—like read their GitHub repositories or post a tweet—without ever seeing their password. You don't want their password. If your database gets compromised, you don't want plain-text or hashed Google passwords sitting on your server. You want a token that says, "Hey, this specific app has permission to read user X's public profile for the next hour."

That token is your valet key.

The Authorization Code Flow (Or: The Dance You Actually Have To Code)

There are several ways OAuth2 can hand over this token (called "Grant Types"). Ignore almost all of them. For 95% of web apps, you only need the Authorization Code Flow.

It’s a four-step handoff between your frontend, your backend, the user, and the identity provider (like Google or GitHub). Here is what actually happens behind the scenes:

  1. The Redirect: Your app tells the user's browser, "Go ask Google if it's okay for us to access their profile." You include your Client ID and a redirect URL.
  2. The Consent Screen: Google asks the user, "Hey, this random app wants to read your profile. Cool?" The user clicks "Allow."
  3. The Code: Google redirects the user back to your site with a temporary string attached to the URL called an authorization_code. This is not the access token yet. It's a single-use receipt.
  4. The Trade: Your backend takes that code, secretly packages it up with your app's client_secret, and sends a direct server-to-server request back to Google. Google verifies it and hands over the actual Access Token.

Why the extra step with the code? Because steps 1 through 3 happen in the user's browser, which is a hostile environment where query params can be logged or intercepted. Step 4 happens entirely on your backend, safely hidden from prying eyes.

Here is what step 4 looks like in Node.js using fetch once you've received that code in your callback route:

async function exchangeCodeForToken(authCode) {
  const response = ares = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      code: authCode,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: 'https://myapp.com/auth/callback',
      grant_type: 'authorization_code',
    }),
  });

  if (!response.ok) {
    const errorBody = await response.text();
    throw new Error(`Failed to exchange token: ${errorBody}`);
  }

  const data = await response.json();
  // data.access_token is what you actually save or use
  return data.access_token;
}
Enter fullscreen mode Exit fullscreen mode

Making the Actual Request

Once you have that access_token, using it is straightforward. You slap it into the Authorization header of an HTTP request.

Let's say you want to fetch the user's profile info from GitHub using the token you just got.

async function fetchGitHubUser(accessToken) {
  const response = await fetch('https://api.github.com/user', {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      // GitHub requires a User-Agent header, or it rejects the request
      'User-Agent': 'my-oauth-learning-app',
    },
  });

  if (!response.ok) {
    throw new Error(`GitHub API returned ${response.status}`);
  }

  const profile = await response.json();
  console.log(`Hello, ${profile.login}!`);
  return profile;
}
Enter fullscreen mode Exit fullscreen mode

That’s it. You don't need to sign the request cryptographically or jump through hoops. You just present the token like a backstage pass.

The Gotchas That Will Trip You Up

When I first built this, I spent two hours debugging an opaque 400 Bad Request error. It turned out to be a mismatch in the trailing slash of my redirect URL. OAuth2 is notoriously unforgiving about string matching.

Here are the mistakes you are likely to make:

  • Exposing the Client Secret: Never, ever put your client_secret in frontend JavaScript, a single-page app running in a browser, or a mobile app binary. Anyone can decompile an app or inspect network requests to steal it. If your app is a pure SPA or mobile app, you should be using PKCE (Proof Key for Code Exchange) instead of a traditional client secret.
  • Redirect URI Mismatch: The URL you send in Step 1 must match the URL you send in Step 4 down to the exact character. If Google expects http://localhost:3000/callback and you send http://127.0.0.1:3000/callback, it will fail silently or throw a cryptic error.
  • Confusing Authentication with Authorization: This is the classic trap. OAuth2 is for authorization (granting permissions). OpenID Connect (OIDC) is built on top of OAuth2 and handles authentication (proving who the user is). When you use "Sign in with Google," you are usually using OIDC, which hands back an id_token (a JWT containing user info) alongside your access token.

Next Steps

Don't try to write an OAuth2 client from scratch for production. Use a well-tested library like Passport.js for Node, Authlib for Python, or let a managed service like Auth0, Supabase, or Firebase handle the edge cases.

To lock this concept in your head today, open up your code editor, pick one provider (GitHub is usually the friendliest for developers), and write a tiny script that successfully logs you into your own local terminal using the Authorization Code Flow.

Top comments (0)