Implementing OAuth2 Authentication

Last Modified: January 1, 2025

OAuth2 is the industry standard for authorization, enabling secure third-party access to resources without sharing credentials. Let's explore how to implement OAuth2 authentication effectively in your applications.

Understanding OAuth2 Fundamentals ๐Ÿ”‘

OAuth2 provides several authorization flows designed for different use cases. The most common flows are:

  1. Authorization Code Flow
  2. Implicit Flow
  3. Client Credentials Flow
  4. Resource Owner Password Flow

Authorization Code Flow Implementation

// server.js
const express = require('express')
const axios = require('axios')

const app = express()

const config = {
    clientId: 'your_client_id',
    clientSecret: 'your_client_secret',
    redirectUri: 'http://localhost:3000/callback'
}

app.get('/auth', (req, res) => {
    const authUrl = `https://oauth-provider.com/auth?
        client_id=${config.clientId}&
        redirect_uri=${config.redirectUri}&
        response_type=code&
        scope=read`

    res.redirect(authUrl)
})

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

    try {
        const tokenResponse = await axios.post('https://oauth-provider.com/token', {
            client_id: config.clientId,
            client_secret: config.clientSecret,
            code,
            grant_type: 'authorization_code',
            redirect_uri: config.redirectUri
        })

        const { access_token, refresh_token } = tokenResponse.data
        // Store tokens securely

        res.redirect('/dashboard')
    } catch (error) {
        res.status(500).json({ error: 'Authentication failed' })
    }
})

Implementing Social Login ๐ŸŒ

Google OAuth2 Integration

// auth/google.js
const { OAuth2Client } = require('google-auth-library')

const client = new OAuth2Client(
    process.env.GOOGLE_CLIENT_ID,
    process.env.GOOGLE_CLIENT_SECRET,
    'http://localhost:3000/auth/google/callback'
)

async function verifyGoogleToken(token) {
    try {
        const ticket = await client.verifyIdToken({
            idToken: token,
            audience: process.env.GOOGLE_CLIENT_ID
        })

        return ticket.getPayload()
    } catch (error) {
        throw new Error('Invalid token')
    }
}

// Usage in Express route
app.post('/auth/google', async (req, res) => {
    try {
        const { token } = req.body
        const userData = await verifyGoogleToken(token)

        // Create or update user in database
        const user = await User.findOrCreate({
            email: userData.email,
            name: userData.name,
            picture: userData.picture
        })

        // Generate JWT
        const jwt = generateToken(user)

        res.json({ token: jwt })
    } catch (error) {
        res.status(401).json({ error: 'Authentication failed' })
    }
})

Facebook OAuth2 Integration

// auth/facebook.js
const axios = require('axios')

async function verifyFacebookToken(accessToken) {
    try {
        const response = await axios.get(
            `https://graph.facebook.com/me?fields=id,name,email&access_token=${accessToken}`
        )

        return response.data
    } catch (error) {
        throw new Error('Invalid Facebook token')
    }
}

app.post('/auth/facebook', async (req, res) => {
    try {
        const { accessToken } = req.body
        const userData = await verifyFacebookToken(accessToken)

        // Process user data
        const user = await User.findOrCreate({
            facebookId: userData.id,
            email: userData.email,
            name: userData.name
        })

        res.json({ token: generateToken(user) })
    } catch (error) {
        res.status(401).json({ error: 'Facebook authentication failed' })
    }
})

Token Management ๐Ÿ”’

JWT Implementation

// utils/jwt.js
const jwt = require('jsonwebtoken')

function generateToken(user) {
    const payload = {
        id: user.id,
        email: user.email,
        roles: user.roles
    }

    return jwt.sign(payload, process.env.JWT_SECRET, {
        expiresIn: '1h'
    })
}

function verifyToken(token) {
    try {
        return jwt.verify(token, process.env.JWT_SECRET)
    } catch (error) {
        throw new Error('Invalid token')
    }
}

// Middleware for protected routes
function authMiddleware(req, res, next) {
    const token = req.headers.authorization?.split(' ')[1]

    if (!token) {
        return res.status(401).json({ error: 'No token provided' })
    }

    try {
        const decoded = verifyToken(token)
        req.user = decoded
        next()
    } catch (error) {
        res.status(401).json({ error: 'Invalid token' })
    }
}

Refresh Token Implementation

// auth/refresh.js
const RefreshToken = require('../models/refreshToken')

async function generateRefreshToken(user) {
    const token = crypto.randomBytes(40).toString('hex')

    await RefreshToken.create({
        token,
        userId: user.id,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
    })

    return token
}

app.post('/auth/refresh', async (req, res) => {
    const { refreshToken } = req.body

    try {
        const tokenDoc = await RefreshToken.findOne({
            token: refreshToken,
            expiresAt: { $gt: new Date() }
        })

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

        const user = await User.findById(tokenDoc.userId)
        const newAccessToken = generateToken(user)

        res.json({ accessToken: newAccessToken })
    } catch (error) {
        res.status(401).json({ error: 'Refresh token invalid' })
    }
})

Security Best Practices ๐Ÿ›ก๏ธ

PKCE Implementation

// auth/pkce.js
const crypto = require('crypto')

function generateCodeVerifier() {
    return crypto.randomBytes(32)
        .toString('base64')
        .replace(/[^a-zA-Z0-9]/g, '')
        .substr(0, 128)
}

function generateCodeChallenge(verifier) {
    return crypto
        .createHash('sha256')
        .update(verifier)
        .digest('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '')
}

// Usage in authorization request
app.get('/auth', (req, res) => {
    const codeVerifier = generateCodeVerifier()
    const codeChallenge = generateCodeChallenge(codeVerifier)

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

    const authUrl = `https://oauth-provider.com/auth?
        client_id=${config.clientId}&
        redirect_uri=${config.redirectUri}&
        response_type=code&
        code_challenge=${codeChallenge}&
        code_challenge_method=S256`

    res.redirect(authUrl)
})

Cross-Site Request Forgery (CSRF) Protection

// middleware/csrf.js
const csrf = require('csurf')

const csrfProtection = csrf({
    cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production'
    }
})

app.use(csrfProtection)

app.get('/auth/csrf-token', (req, res) => {
    res.json({ csrfToken: req.csrfToken() })
})

Error Handling

// middleware/errorHandler.js
class AuthenticationError extends Error {
    constructor(message) {
        super(message)
        this.name = 'AuthenticationError'
        this.statusCode = 401
    }
}

function errorHandler(err, req, res, next) {
    if (err instanceof AuthenticationError) {
        return res.status(err.statusCode).json({
            error: err.message
        })
    }

    // Handle other errors
    res.status(500).json({
        error: 'Internal server error'
    })
}

app.use(errorHandler)

Testing OAuth2 Implementation ๐Ÿงช

// tests/auth.test.js
const request = require('supertest')
const app = require('../app')
const { generateTestToken } = require('./helpers')

describe('OAuth2 Authentication', () => {
    test('should authenticate with valid Google token', async () => {
        const mockToken = generateTestToken()

        const response = await request(app)
            .post('/auth/google')
            .send({ token: mockToken })

        expect(response.status).toBe(200)
        expect(response.body).toHaveProperty('token')
    })

    test('should refresh access token', async () => {
        const refreshToken = 'valid-refresh-token'

        const response = await request(app)
            .post('/auth/refresh')
            .send({ refreshToken })

        expect(response.status).toBe(200)
        expect(response.body).toHaveProperty('accessToken')
    })
})

Frontend Integration

// auth/client.js
class AuthClient {
    async login(provider) {
        const popup = window.open(
            `/auth/${provider}`,
            'Auth Popup',
            'width=500,height=600'
        )

        return new Promise((resolve, reject) => {
            window.addEventListener('message', (event) => {
                if (event.data.type === 'auth_complete') {
                    popup.close()
                    resolve(event.data.token)
                }
            })
        })
    }

    async refreshToken() {
        const refreshToken = localStorage.getItem('refreshToken')

        const response = await fetch('/auth/refresh', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ refreshToken })
        })

        const data = await response.json()
        return data.accessToken
    }
}

Best Practices ๐Ÿ“

  1. Secure Token Storage
// Never store sensitive tokens in localStorage
const secureStorage = {
    setToken(token) {
        document.cookie = `auth_token=${token}; Secure; HttpOnly; SameSite=Strict`
    },

    getToken() {
        return document.cookie
            .split('; ')
            .find(row => row.startsWith('auth_token='))
            ?.split('=')[1]
    }
}
  1. Rate Limiting
const rateLimit = require('express-rate-limit')

const authLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 5 // limit each IP to 5 requests per windowMs
})

app.use('/auth', authLimiter)
  1. Logging
const winston = require('winston')

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'auth.log' })
    ]
})

// Log authentication attempts
app.use((req, res, next) => {
    logger.info('Authentication attempt', {
        ip: req.ip,
        path: req.path,
        timestamp: new Date()
    })
    next()
})

Conclusion

Implementing OAuth2 authentication requires careful attention to security and best practices. Remember to:

  • Use HTTPS in production
  • Implement proper token management
  • Handle errors gracefully
  • Protect against common security vulnerabilities
  • Follow OAuth2 specifications
  • Test thoroughly
  • Monitor and log authentication attempts

These practices will help you build a secure and reliable authentication system for your applications.