NodeJSOAuth 2.0 & Social Login

OAuth 2.0 & Social Login

OAuth 2.0 is an authorization framework that lets your app act on behalf of a user at an external service — or, in the social-login case, lets a provider (Google, GitHub, etc.) vouch for a user's identity so your app doesn't need to manage credentials at all. The user authenticates with Google, Google hands your app a code, your server exchanges it for tokens, you get the user's profile, and you find or create a local account. This page covers the authorization-code flow, the security parameters that keep it safe, and implementation with Passport.js.

The authorization-code flow

Text
1. Your app redirects the user to the provider's authorization URL:
     https://accounts.google.com/o/oauth2/auth?
       client_id=...&redirect_uri=...&scope=email+profile&state=<random>

2. User logs into Google and approves access.

3. Provider redirects back to YOUR callback URL with a code:
     https://yourapp.com/auth/google/callback?code=<one-time-code>&state=<same-random>

4. Your server exchanges the code for tokens (back-channel, never exposed to the browser):
     POST https://oauth2.googleapis.com/token  { code, client_id, client_secret, ... }
     → { access_token, id_token, refresh_token }

5. Use the access_token to call the provider's API for the user's profile.

6. Find or create the user in your own database; issue YOUR session or JWT.
The code exchange happens server-to-server — the access token never touches the browser
The critical security feature of the authorization-code flow is step 4: the one-time code received by the browser is **exchanged server-side** for the actual access token. The access token never passes through the user's browser, so it can't be intercepted by a network observer or extracted from browser history. The alternative — the implicit flow — handed the token directly to the browser and is now deprecated in OAuth 2.1 precisely because it was less safe. Always use the authorization-code flow.
The `state` parameter prevents CSRF
Always generate and verify a `state` parameter — it stops CSRF on the callback
Without `state`, an attacker can trick your user's browser into hitting your callback URL with the attacker's `code`, linking the attacker's provider account to your user's account (account takeover). The fix: generate a random `state` value, store it in the session, pass it to the provider in the redirect, and **verify it matches** when the callback arrives. Passport strategies do this for you when configured correctly — but understand what it's protecting against.
Passport Google strategy

Bash
npm install passport-google-oauth20

JS
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'

passport.use(new GoogleStrategy(
  {
    clientID:     process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL:  'https://yourapp.com/auth/google/callback',
    scope: ['email', 'profile'],
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Find or create a local user record linked to this provider identity:
      let user = await db.users.findByProvider('google', profile.id)
      if (!user) {
        user = await db.users.create({
          email:       profile.emails[0].value,
          name:        profile.displayName,
          provider:    'google',
          providerId:  profile.id,
        })
      }
      return done(null, user)
    } catch (err) {
      return done(err)
    }
  },
))

Routes

JS
// Redirect user to Google:
app.get('/auth/google', passport.authenticate('google'))

// Google redirects back here after the user approves:
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => res.redirect('/dashboard'),  // success
)
Find-or-create: linking provider identity to your account
Store the provider name + provider id — not just the email — as the link key
Emails aren't always stable: users can change them, and the same email might be a different account at different providers. Link by **provider + providerId** (the opaque unique id the provider assigns) rather than by email alone. Email can serve as a display/contact field, but your database's `provider`/`providerId` pair is the reliable join key. If you support multiple providers per account, let users link several rows to one user record.
Registering your app with providers

Provider

Console URL

Env vars

Google

console.cloud.google.com → OAuth 2.0

GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET

GitHub

github.com/settings/developers

GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET

Facebook

developers.facebook.com

FACEBOOK_APP_ID, FACEBOOK_APP_SECRET

Client secret stays server-side — never expose it to the browser or commit it to git
The `clientSecret` is what authenticates **your server** to the provider's token endpoint. If it leaks, anyone can impersonate your application. Load it from an environment variable or secrets manager, never from source code. The `clientID` is not secret (it appears in the redirect URL), but the `clientSecret` absolutely is. Rotate it immediately if you suspect exposure.
OAuth scopes — request only what you need
  • Request minimal scopesemail and profile for login; don't ask for write access if you only read.

  • Inform users what access they are granting — users reject over-reaching apps.

  • Re-request additional scopes incrementally when a feature actually needs them.

  • The access token from OAuth is for calling that provider's API, not your own; issue your own session/JWT after login.

The provider's access token gives access to THEIR API, not yours
After a successful OAuth flow you hold a Google (or GitHub, etc.) access token. That token works on Google's APIs — it does not authenticate the user to *your* API. Issue your own [session](/nodejs/sessions) or [JWT](/nodejs/jwt) from the `verify` callback, and use that for all subsequent requests to your own endpoints. The provider token should stay on the server; it's a tool for fetching the user's profile at login time, not a bearer credential for your app.
PKCE for public clients
Single-page apps and mobile apps should use PKCE — they can't keep a secret
A server-side app can keep `clientSecret` private. A single-page app or mobile app cannot — the secret would be visible in the bundle. **PKCE** (Proof Key for Code Exchange) solves this: the client generates a random `code_verifier`, hashes it to a `code_challenge`, includes the challenge in the authorization request, and sends the verifier at token exchange time. The provider verifies they match, without needing a client secret. For SPAs and mobile apps, PKCE is the correct OAuth flow. Most provider SDKs and libraries handle it for you.
Next
Control what authenticated users can do: [Role-Based Access Control](/nodejs/rbac).