How to Implement OAuth in Your Web Application

15 min read
OAuth
Authentication
Security
Web Development
API
JWT
Next.js
Node.js
SC
Written by Shailesh Chaudhari
Full-Stack Developer & Problem Solver
TL;DR: Complete guide to implementing OAuth 2.0 in web applications. Learn to integrate with Google, GitHub, and Facebook, handle authentication flows, manage tokens securely, and build production-ready auth systems.

Understanding OAuth 2.0

Hello everyone! I'm Shailesh Chaudhari, a full-stack developer who has implemented authentication systems for various web applications. Today, I'll guide you through implementing OAuth 2.0 authentication in your web applications.

OAuth 2.0 is the industry-standard protocol for authorization, allowing users to grant third-party applications access to their resources without sharing their credentials. It's commonly used for "Login with Google/Facebook/GitHub" functionality.

OAuth 2.0 Roles

  • Resource Owner: The user who owns the data
  • Client: Your application requesting access
  • Authorization Server: Issues access tokens (e.g., Google)
  • Resource Server: Hosts the protected resources

OAuth 2.0 Flow Types

Authorization Code Flow (Most Secure)

The authorization code flow is the most secure and recommended for web applications:

  1. User clicks "Login with Google"
  2. App redirects to Google's authorization endpoint
  3. User grants permission
  4. Google redirects back with authorization code
  5. App exchanges code for access token
  6. App uses token to access user data

Implicit Flow (Legacy)

Used for client-side applications, but less secure than authorization code flow.

Backend Setup (Node.js/Express)

Installing Dependencies

npm install express passport passport-google-oauth20 passport-github2 passport-facebook express-session
npm install --save-dev @types/passport @types/express-session

Passport Configuration

// lib/auth.ts
import passport from 'passport'
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
import { Strategy as GitHubStrategy } from 'passport-github2'
import { Strategy as FacebookStrategy } from 'passport-facebook'

// Serialize user for session
passport.serializeUser((user: any, done) => {
  done(null, user.id)
})

passport.deserializeUser(async (id: string, done) => {
  try {
    // Fetch user from database
    const user = await User.findById(id)
    done(null, user)
  } catch (error) {
    done(error, null)
  }
})

// Google Strategy
passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
  try {
    // Find or create user
    let user = await User.findOne({ googleId: profile.id })

    if (!user) {
      user = await User.create({
        googleId: profile.id,
        email: profile.emails?.[0].value,
        name: profile.displayName,
        avatar: profile.photos?.[0].value,
        provider: 'google'
      })
    }

    done(null, user)
  } catch (error) {
    done(error, null)
  }
}))

// GitHub Strategy
passport.use(new GitHubStrategy({
  clientID: process.env.GITHUB_CLIENT_ID!,
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
  callbackURL: '/auth/github/callback'
}, async (accessToken, refreshToken, profile, done) => {
  try {
    let user = await User.findOne({ githubId: profile.id })

    if (!user) {
      user = await User.create({
        githubId: profile.id,
        email: profile.emails?.[0].value,
        name: profile.displayName,
        avatar: profile.photos?.[0].value,
        provider: 'github'
      })
    }

    done(null, user)
  } catch (error) {
    done(error, null)
  }
}))

// Facebook Strategy
passport.use(new FacebookStrategy({
  clientID: process.env.FACEBOOK_APP_ID!,
  clientSecret: process.env.FACEBOOK_APP_SECRET!,
  callbackURL: '/auth/facebook/callback',
  profileFields: ['id', 'emails', 'name', 'picture']
}, async (accessToken, refreshToken, profile, done) => {
  try {
    let user = await User.findOne({ facebookId: profile.id })

    if (!user) {
      user = await User.create({
        facebookId: profile.id,
        email: profile.emails?.[0].value,
        name: profile.displayName,
        avatar: profile.photos?.[0].value,
        provider: 'facebook'
      })
    }

    done(null, user)
  } catch (error) {
    done(error, null)
  }
}))

export default passport

Express Server Setup

// server.ts
import express from 'express'
import session from 'express-session'
import passport from './lib/auth'

const app = express()

// Session configuration
app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}))

// Passport middleware
app.use(passport.initialize())
app.use(passport.session())

// Auth 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('/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
)

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

app.get('/auth/facebook',
  passport.authenticate('facebook', { scope: ['email'] })
)

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

// Logout
app.post('/auth/logout', (req, res) => {
  req.logout((err) => {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' })
    }
    res.redirect('/')
  })
})

// Get current user
app.get('/auth/user', (req, res) => {
  if (req.user) {
    res.json({ user: req.user })
  } else {
    res.status(401).json({ error: 'Not authenticated' })
  }
})

const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

Frontend Implementation (Next.js)

OAuth Login Component

// components/OAuthLogin.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

interface OAuthLoginProps {
  onSuccess?: () => void
  onError?: (error: string) => void
}

export default function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
  const [loading, setLoading] = useState<string | null>(null)
  const router = useRouter()

  const handleOAuthLogin = async (provider: string) => {
    setLoading(provider)

    try {
      // For Next.js API routes
      window.location.href = `/api/auth/${provider}`

      // Alternative: Open popup for better UX
      // const popup = window.open(
      //   `/api/auth/${provider}`,
      //   'oauth-popup',
      //   'width=500,height=600'
      // )

      // if (popup) {
      //   const checkClosed = setInterval(() => {
      //     if (popup.closed) {
      //       clearInterval(checkClosed)
      //       // Check authentication status
      //       router.refresh()
      //       onSuccess?.()
      //     }
      //   }, 1000)
      // }
    } catch (error) {
      setLoading(null)
      onError?.('Authentication failed')
    }
  }

  return (
    <div className="space-y-4">
      <button
        onClick={() => handleOAuthLogin('google')}
        disabled={!!loading}
        className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
      >
        {loading === 'google' ? (
          <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-600 mr-2"></div>
        ) : (
          <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
            <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
            <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
            <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
            <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
          </svg>
        )}
        Continue with Google
      </button>

      <button
        onClick={() => handleOAuthLogin('github')}
        disabled={!!loading}
        className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
      >
        {loading === 'github' ? (
          <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
        ) : (
          <svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
            <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
          </svg>
        )}
        Continue with GitHub
      </button>

      <button
        onClick={() => handleOAuthLogin('facebook')}
        disabled={!!loading}
        className="w-full flex items-center justify-center px-4 py-3 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
      >
        {loading === 'facebook' ? (
          <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
        ) : (
          <svg className="w-5 h-5 mr-2" fill="#1877F2" viewBox="0 0 24 24">
            <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
          </svg>
        )}
        Continue with Facebook
      </button>
    </div>
  )
}

Next.js API Routes

// pages/api/auth/[...nextauth].ts (NextAuth.js alternative)
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import GitHubProvider from 'next-auth/providers/github'
import FacebookProvider from 'next-auth/providers/facebook'

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    FacebookProvider({
      clientId: process.env.FACEBOOK_APP_ID!,
      clientSecret: process.env.FACEBOOK_APP_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, account, profile }) {
      if (account) {
        token.accessToken = account.access_token
      }
      return token
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken
      return session
    },
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/error',
  },
})

JWT Token Management

Custom JWT Implementation

// lib/jwt.ts
import jwt from 'jsonwebtoken'

const JWT_SECRET = process.env.JWT_SECRET!
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!

export interface JWTPayload {
  userId: string
  email: string
  provider: string
  iat?: number
  exp?: number
}

export function generateAccessToken(payload: Omit<JWTPayload, 'iat' | 'exp'>): string {
  return jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' })
}

export function generateRefreshToken(payload: Omit<JWTPayload, 'iat' | 'exp'>): string {
  return jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: '7d' })
}

export function verifyAccessToken(token: string): JWTPayload | null {
  try {
    return jwt.verify(token, JWT_SECRET) as JWTPayload
  } catch (error) {
    return null
  }
}

export function verifyRefreshToken(token: string): JWTPayload | null {
  try {
    return jwt.verify(token, JWT_REFRESH_SECRET) as JWTPayload
  } catch (error) {
    return null
  }
}

// Refresh token rotation
export async function refreshAccessToken(refreshToken: string) {
  const payload = verifyRefreshToken(refreshToken)

  if (!payload) {
    throw new Error('Invalid refresh token')
  }

  // Generate new tokens
  const newAccessToken = generateAccessToken({
    userId: payload.userId,
    email: payload.email,
    provider: payload.provider,
  })

  const newRefreshToken = generateRefreshToken({
    userId: payload.userId,
    email: payload.email,
    provider: payload.provider,
  })

  return {
    accessToken: newAccessToken,
    refreshToken: newRefreshToken,
  }
}

Token Storage and Management

// lib/auth-storage.ts
export class AuthStorage {
  private static ACCESS_TOKEN_KEY = 'access_token'
  private static REFRESH_TOKEN_KEY = 'refresh_token'

  static setTokens(accessToken: string, refreshToken: string) {
    if (typeof window !== 'undefined') {
      localStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken)
      localStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken)
    }
  }

  static getAccessToken(): string | null {
    if (typeof window !== 'undefined') {
      return localStorage.getItem(this.ACCESS_TOKEN_KEY)
    }
    return null
  }

  static getRefreshToken(): string | null {
    if (typeof window !== 'undefined') {
      return localStorage.getItem(this.REFRESH_TOKEN_KEY)
    }
    return null
  }

  static clearTokens() {
    if (typeof window !== 'undefined') {
      localStorage.removeItem(this.ACCESS_TOKEN_KEY)
      localStorage.removeItem(this.REFRESH_TOKEN_KEY)
    }
  }

  static isAuthenticated(): boolean {
    const token = this.getAccessToken()
    if (!token) return false

    try {
      const payload = JSON.parse(atob(token.split('.')[1]))
      return payload.exp * 1000 > Date.now()
    } catch {
      return false
    }
  }
}

Security Best Practices

State Parameter Protection

// Prevent CSRF attacks
app.get('/auth/google', (req, res) => {
  const state = crypto.randomBytes(32).toString('hex')
  req.session.oauthState = state

  passport.authenticate('google', {
    scope: ['profile', 'email'],
    state: state
  })(req, res)
})

app.get('/auth/google/callback', (req, res) => {
  if (req.query.state !== req.session.oauthState) {
    return res.status(403).json({ error: 'Invalid state parameter' })
  }

  passport.authenticate('google', { failureRedirect: '/login' })(req, res)
})

PKCE for Public Clients

// Proof Key for Code Exchange
function generateCodeVerifier() {
  return crypto.randomBytes(32).toString('base64url')
}

function generateCodeChallenge(verifier: string) {
  const hash = crypto.createHash('sha256').update(verifier).digest()
  return hash.toString('base64url')
}

// Usage in authorization request
const codeVerifier = generateCodeVerifier()
const codeChallenge = generateCodeChallenge(codeVerifier)

// Store codeVerifier in session
req.session.codeVerifier = codeVerifier

// Include in authorization URL
const authUrl = `https://accounts.google.com/oauth/authorize?...${codeChallenge}...`

Secure Cookie Configuration

// Session configuration for production
app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only
    httpOnly: true, // Prevent XSS
    sameSite: 'strict', // CSRF protection
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  },
  store: new RedisStore({ // Use Redis in production
    client: redisClient,
    ttl: 24 * 60 * 60
  })
}))

OAuth Provider Setup

Google OAuth Setup

  1. Go to Google Cloud Console
  2. Create a new project or select existing one
  3. Enable Google+ API
  4. Create OAuth 2.0 credentials
  5. Add authorized redirect URIs
  6. Configure consent screen

GitHub OAuth Setup

  1. Go to GitHub Settings → Developer settings → OAuth Apps
  2. Click "New OAuth App"
  3. Fill in application details
  4. Add authorization callback URL
  5. Copy Client ID and Client Secret

Facebook OAuth Setup

  1. Go to Facebook Developers
  2. Create a new app
  3. Add Facebook Login product
  4. Configure OAuth redirect URIs
  5. Get App ID and App Secret

Handling OAuth Errors

Error Types and Solutions

// OAuth error handling middleware
app.use('/auth', (error, req, res, next) => {
  if (error.code === 'access_denied') {
    return res.redirect('/login?error=access_denied')
  }

  if (error.code === 'invalid_request') {
    return res.redirect('/login?error=invalid_request')
  }

  if (error.code === 'unauthorized_client') {
    return res.redirect('/login?error=unauthorized_client')
  }

  // Log error and redirect to generic error page
  console.error('OAuth error:', error)
  res.redirect('/login?error=oauth_error')
})

Frontend Error Handling

// components/AuthError.tsx
'use client'

import { useSearchParams } from 'next/navigation'

const errorMessages = {
  access_denied: 'Access was denied by the OAuth provider.',
  invalid_request: 'Invalid authorization request.',
  unauthorized_client: 'Unauthorized client application.',
  oauth_error: 'An error occurred during authentication.',
}

export default function AuthError() {
  const searchParams = useSearchParams()
  const error = searchParams.get('error')

  if (!error || !errorMessages[error as keyof typeof errorMessages]) {
    return null
  }

  return (
    <div className="bg-red-50 border border-red-200 rounded-md p-4 mb-4">
      <div className="flex">
        <div className="ml-3">
          <h3 className="text-sm font-medium text-red-800">
            Authentication Error
          </h3>
          <div className="mt-2 text-sm text-red-700">
            {errorMessages[error as keyof typeof errorMessages]}
          </div>
        </div>
      </div>
    </div>
  )
}

Testing OAuth Implementation

Development Environment Setup

# .env.local
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
SESSION_SECRET=your_session_secret
JWT_SECRET=your_jwt_secret
JWT_REFRESH_SECRET=your_jwt_refresh_secret

Testing Different Scenarios

  • Successful authentication: Complete OAuth flow
  • User denies access: Handle access_denied error
  • Invalid credentials: Test with wrong client secrets
  • Token expiration: Test refresh token flow
  • Network errors: Simulate connection issues
  • State parameter mismatch: Test CSRF protection

Production Deployment

Environment Variables

# Production environment variables
NODE_ENV=production
SESSION_SECRET=strong_random_secret_here
JWT_SECRET=another_strong_secret
JWT_REFRESH_SECRET=yet_another_secret

# OAuth credentials (use production apps)
GOOGLE_CLIENT_ID=prod_google_client_id
GOOGLE_CLIENT_SECRET=prod_google_client_secret
# ... other provider credentials

Security Checklist

  • ✅ Use HTTPS in production
  • ✅ Store secrets securely (not in code)
  • ✅ Implement proper session management
  • ✅ Use secure cookies
  • ✅ Validate redirect URIs
  • ✅ Implement rate limiting
  • ✅ Log authentication events
  • ✅ Regular security audits

Advanced OAuth Features

Scope Management

// Request specific permissions
app.get('/auth/google/calendar',
  passport.authenticate('google', {
    scope: [
      'https://www.googleapis.com/auth/calendar.readonly',
      'https://www.googleapis.com/auth/userinfo.email'
    ]
  })
)

// Handle incremental authorization
app.get('/auth/google/incremental', (req, res) => {
  const scopes = req.query.scopes?.split(',') || ['profile', 'email']

  passport.authenticate('google', {
    scope: scopes,
    accessType: 'offline', // For refresh tokens
    prompt: 'consent' // Force consent screen
  })(req, res)
})

Multi-Provider Account Linking

// Link multiple OAuth accounts to one user
app.post('/auth/link-provider', async (req, res) => {
  const { provider, code } = req.body
  const userId = req.user.id

  try {
    // Exchange code for token and get user info
    const tokenData = await exchangeCodeForToken(provider, code)
    const providerUser = await getProviderUserInfo(provider, tokenData.access_token)

    // Link to existing user account
    await User.findByIdAndUpdate(userId, {
      $set: {
        [`${provider}Id`]: providerUser.id,
        [`${provider}Profile`]: providerUser
      }
    })

    res.json({ success: true, linked: provider })
  } catch (error) {
    res.status(500).json({ error: 'Failed to link account' })
  }
})

Conclusion: Building Secure OAuth Systems

Implementing OAuth 2.0 authentication in your web application doesn't have to be complex. By following the patterns and best practices outlined in this guide, you can build secure, scalable authentication systems that integrate seamlessly with major OAuth providers.

Start with the fundamentals—choose the right OAuth flow for your application, implement proper security measures, and handle errors gracefully. As your application grows, you can add advanced features like account linking, incremental authorization, and custom scopes.

Remember that authentication is a critical security component of your application. Always prioritize security best practices, keep your dependencies updated, and regularly audit your implementation for vulnerabilities.

The key to successful OAuth implementation is understanding the protocol, implementing proper security measures, and providing a smooth user experience. With these foundations in place, you can confidently handle user authentication at any scale.

"Authentication is not just about security—it's about building trust with your users." - Shailesh Chaudhari

SC
Written by Shailesh Chaudhari
Full-Stack Developer & Problem Solver