OAuth 2.0 — Complete Guide for Developers

Sanjeev SharmaSanjeev Sharma
3 min read

Advertisement

OAuth 2.0 is the industry standard for delegated authentication. Let users login with Google, GitHub, etc.

OAuth 2.0 Flow

Authorization Code flow:

  1. User clicks "Login with Google"
  2. App redirects to Google's authorization server
  3. User approves permissions
  4. Google redirects back with authorization code
  5. App exchanges code for access token
  6. App uses token to get user info

Implementation with Passport.js

npm install passport passport-google-oauth20 express-session
import passport from "passport";
import GoogleStrategy from "passport-google-oauth20";
import session from "express-session";

app.use(
  session({
    secret: "your-secret",
    resave: false,
    saveUninitialized: false,
  })
);

app.use(passport.initialize());
app.use(passport.session());

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      callbackURL: "http://localhost:3000/auth/google/callback",
    },
    async (accessToken, refreshToken, profile, done) => {
      let user = await db.users.findOne({ googleId: profile.id });
      if (!user) {
        user = await db.users.create({
          googleId: profile.id,
          email: profile.emails[0].value,
          name: profile.displayName,
        });
      }
      return done(null, user);
    }
  )
);

passport.serializeUser((user: any, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id: number, done) => {
  const user = await db.users.findById(id);
  done(null, user);
});

// Routes
app.get(
  "/auth/google",
  passport.authenticate("google", { scope: ["profile", "email"] })
);

app.get(
  "/auth/google/callback",
  passport.authenticate("google", { failureRedirect: "/login" }),
  (req, res) => {
    res.redirect("/dashboard");
  }
);

app.get("/logout", (req, res) => {
  req.logout(() => res.redirect("/"));
});

Manual OAuth Implementation

// Redirect to provider
app.get("/login/github", (req, res) => {
  const params = new URLSearchParams({
    client_id: process.env.GITHUB_CLIENT_ID!,
    redirect_uri: "http://localhost:3000/auth/github/callback",
    scope: "user:email",
    state: generateState(), // CSRF protection
  });
  res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});

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

  if (!validateState(state as string)) {
    return res.status(400).json({ error: "Invalid state" });
  }

  // Exchange code for token
  const tokenResponse = 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,
      }),
    }
  );

  const { access_token } = await tokenResponse.json();

  // Get user info
  const userResponse = await fetch("https://api.github.com/user", {
    headers: { Authorization: `Bearer ${access_token}` },
  });

  const profile = await userResponse.json();

  // Find or create user
  let user = await db.users.findOne({ githubId: profile.id });
  if (!user) {
    user = await db.users.create({
      githubId: profile.id,
      email: profile.email,
      name: profile.name,
    });
  }

  // Generate JWT
  const token = jwt.sign({ userId: user.id }, secret, { expiresIn: "24h" });
  res.json({ token });
});

PKCE (for mobile/SPAs)

// Generate code verifier
function generateCodeVerifier() {
  const bytes = crypto.randomBytes(32);
  return Buffer.from(bytes).toString("base64url");
}

// Generate code challenge
function generateCodeChallenge(verifier: string) {
  const hash = crypto.createHash("sha256").update(verifier).digest();
  return Buffer.from(hash).toString("base64url");
}

// Use in authorization request
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);
localStorage.setItem("code_verifier", verifier);

// Exchange code with verifier
await fetch("https://provider.com/token", {
  method: "POST",
  body: JSON.stringify({
    grant_type: "authorization_code",
    code,
    code_verifier: localStorage.getItem("code_verifier"),
  }),
});

Scopes and Permissions

// Request specific scopes
const scopes = ["user:email", "repo", "gist"];

passport.use(
  new GitHubStrategy(
    {
      clientID: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      scope: scopes,
    },
    verify
  )
);

FAQ

Q: Is OAuth 2.0 secure? A: Yes, if implemented correctly. Always use HTTPS, validate state, protect secrets.

Q: Should I still support passwords? A: Many users prefer OAuth. Support both for flexibility.

Q: What's the difference between OAuth and OpenID Connect? A: OAuth is for authorization. OpenID Connect adds authentication on top.


OAuth 2.0 enables seamless social login experiences. Master it for modern authentication.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro