100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Your First OAuth Integration

A hands-on walkthrough of building a 'Sign in with GitHub' integration end to end: app registration, the redirect, the callback handler, and using the resulting token safely.

FoundationsIntermediate11 min readJul 10, 2026
Analogies

Your First OAuth Integration

This walkthrough builds a real, minimal 'Sign in with GitHub' feature for a small Node.js/Express backend, using the authorization code flow covered earlier. The goal is to go from zero to a working callback route that logs a user in, so you can see how the abstract roles and terms (client, authorization server, redirect_uri, state, tokens) map onto actual code you'd write and ship, including the parts that are easy to get wrong the first time — like validating state and never leaking the client_secret.

🏏

Cricket analogy: This is like moving from studying the laws of cricket in a textbook to actually walking out to the middle for your first net session — the rules about LBW and no-balls only click once you're facing a real ball.

Step 1: Register Your Application

Every real integration starts on the provider's developer settings page — for GitHub, that's Settings → Developer settings → OAuth Apps → New OAuth App. You'll provide a homepage URL and, critically, an 'Authorization callback URL' that must exactly match the redirect_uri your code sends later (for local development this is typically http://localhost:3000/auth/github/callback). Registering returns a client_id (safe to expose in frontend code or a public repo) and a client_secret (which must go into an environment variable or secrets manager, never into version control).

🏏

Cricket analogy: This is like a team formally registering its squad list and home ground address with the league office before the season starts — you can't just show up and play; the venue and roster have to be filed and approved in advance.

Step 2: Build the Authorization Request and Callback

The login button's link should point to your own /auth/github/login route, not directly at GitHub — this gives your server a chance to generate a fresh, random state value, store it in the user's session, and only then build the full https://github.com/login/oauth/authorize URL with client_id, redirect_uri, scope=read:user, and that state. Building the URL server-side like this (rather than hardcoding it in frontend HTML) keeps the state generation tied to an actual server-side session you can later verify.

🏏

Cricket analogy: This is like a fan not walking directly up to the stadium gate, but first stopping at the club's own ticket office to get a numbered token tied to their registered membership, which is only then presented at the gate.

On the callback route, the very first thing your handler must do is compare the state query parameter against the value stored in the session, rejecting the request outright if they don't match. Only after that check passes should you take the code and make the server-to-server POST to https://github.com/login/oauth/access_token, parse the resulting access token from the response, and use it to call https://api.github.com/user to fetch the user's profile before creating your own application session (a cookie, JWT, or similar) — the GitHub access token itself should typically not be handed to the frontend.

🏏

Cricket analogy: This is like the gate steward checking the numbered stub against the office's own log before letting anyone in, and only then radioing the ticket office to confirm the seat — skipping the stub check first would let anyone with a lookalike stub walk straight through.

javascript
// server.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();

app.get('/auth/github/login', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  req.session.oauthState = state;

  const params = new URLSearchParams({
    client_id: process.env.GITHUB_CLIENT_ID,
    redirect_uri: 'http://localhost:3000/auth/github/callback',
    scope: 'read:user',
    state,
  });
  res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});

app.get('/auth/github/callback', async (req, res) => {
  const { code, state } = req.query;

  if (!state || state !== req.session.oauthState) {
    return res.status(400).send('Invalid state parameter');
  }
  delete req.session.oauthState;

  const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
    method: 'POST',
    headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: process.env.GITHUB_CLIENT_ID,
      client_secret: process.env.GITHUB_CLIENT_SECRET,
      code,
      redirect_uri: 'http://localhost:3000/auth/github/callback',
    }),
  });
  const { access_token, error } = await tokenRes.json();
  if (error || !access_token) {
    return res.status(401).send('OAuth token exchange failed');
  }

  const userRes = await fetch('https://api.github.com/user', {
    headers: { Authorization: `Bearer ${access_token}` },
  });
  const githubUser = await userRes.json();

  req.session.userId = githubUser.id;
  req.session.username = githubUser.login;
  res.redirect('/dashboard');
});

Step 3: Use the Token and Handle Errors

Once you have an access token, every subsequent API call should send it in the Authorization: Bearer header and be wrapped in explicit error handling for the two failure modes you'll hit constantly in production: a 401 response meaning the token expired or was revoked (requiring a refresh-token exchange, or if there's no refresh token, a fresh login), and a 403 response meaning the token is valid but lacks the scope needed for that specific call. Treating these as the same generic 'API error' makes debugging integrations painful, so most production code branches on the status code explicitly.

🏏

Cricket analogy: This is like a scorer distinguishing between 'the umpire's radio has lost signal entirely' (401 — needs reconnecting) versus 'the radio works fine but this channel isn't licensed for boundary review footage' (403 — right connection, wrong permission) — treating both the same wastes the whole review.

Store OAuth tokens the way you'd store any other secret: encrypted at rest if persisted server-side, never in localStorage if you can avoid it (localStorage is readable by any JavaScript on the page, including injected via XSS), and prefer an HttpOnly, Secure session cookie for your own app's session once the OAuth handshake is done.

Never commit a client_secret to a git repository, even a private one — it will end up in history forever and in CI logs. Load it from an environment variable or a secrets manager, and add a git-ignored .env file for local development, exactly as this walkthrough's server.js does with process.env.GITHUB_CLIENT_SECRET.

  • Registering an OAuth app requires declaring an exact redirect URI in advance and yields a client_id and client_secret.
  • The login route should be built server-side so a fresh, session-bound state value is generated per request.
  • The callback handler must verify state before doing anything else, rejecting mismatches outright.
  • The code-for-token exchange happens server-to-server, keeping the client_secret out of the browser.
  • The provider's raw access token should generally stay server-side; issue your own app session instead.
  • Distinguish 401 (expired/invalid token) from 403 (valid token, insufficient scope) in error handling.
  • Never commit client_secret to version control; load it from environment variables or a secrets manager.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#OAuth20StudyNotes#YourFirstOAuthIntegration#OAuth#Integration#Step#Register#StudyNotes#SkillVeris#ExamPrep