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:
- Authorization Code Flow
- Implicit Flow
- Client Credentials Flow
- 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 ๐
- 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]
}
}
- 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)
- 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.