OAuth2 is the industry-standard protocol for authorization. In this guide, we'll walk through implementing OAuth2 in a Node.js application, covering everything from basic concepts to advanced security considerations.
Understanding OAuth2 Flows 🔑
OAuth2 provides several authorization flows, each designed for specific use cases:
- Authorization Code Flow
- Implicit Flow
- Client Credentials Flow
- Resource Owner Password Flow
Let's implement the most common and secure flow: Authorization Code Flow.
Setting Up the Project
First, create a new Node.js project and install the required dependencies:
npm init -y
npm install express passport passport-oauth2 dotenv express-session
Create the basic server structure:
require('dotenv').config()
const express = require('express')
const session = require('express-session')
const passport = require('passport')
const OAuth2Strategy = require('passport-oauth2')
const app = express()
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}))
app.use(passport.initialize())
app.use(passport.session())
Implementing OAuth2 Strategy 🛠️
1. Configure Passport Strategy
passport.use(new OAuth2Strategy({
authorizationURL: 'https://provider.com/oauth/authorize',
tokenURL: 'https://provider.com/oauth/token',
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/callback'
},
function(accessToken, refreshToken, profile, cb) {
User.findOrCreate({ oauthId: profile.id }, function (err, user) {
return cb(err, user)
})
}))
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user)
})
})
2. Create Authentication Routes
app.get('/auth/login', passport.authenticate('oauth2'))
app.get('/auth/callback',
passport.authenticate('oauth2', {
failureRedirect: '/login'
}),
(req, res) => {
res.redirect('/')
}
)
app.get('/auth/logout', (req, res) => {
req.logout()
res.redirect('/')
})
Protecting Routes
Create a middleware to check authentication:
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next()
}
res.redirect('/auth/login')
}
app.get('/protected',
ensureAuthenticated,
(req, res) => {
res.json({ user: req.user })
}
)
Token Management 🔄
1. Access Token Storage
Create a secure token manager:
class TokenManager {
constructor() {
this.tokens = new Map()
}
storeToken(userId, tokens) {
this.tokens.set(userId, {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: Date.now() + (tokens.expiresIn * 1000)
})
}
async getValidToken(userId) {
const tokens = this.tokens.get(userId)
if (!tokens) return null
if (Date.now() >= tokens.expiresAt) {
return await this.refreshToken(userId, tokens.refreshToken)
}
return tokens.accessToken
}
async refreshToken(userId, refreshToken) {
try {
const response = await fetch('https://provider.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET
})
})
const newTokens = await response.json()
this.storeToken(userId, newTokens)
return newTokens.access_token
} catch (error) {
console.error('Error refreshing token:', error)
return null
}
}
}
2. Using the Token Manager
const tokenManager = new TokenManager()
app.get('/api/resource', async (req, res) => {
const token = await tokenManager.getValidToken(req.user.id)
if (!token) {
return res.status(401).json({ error: 'Invalid token' })
}
// Make API request with token
const response = await fetch('https://api.provider.com/resource', {
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await response.json()
res.json(data)
})
Error Handling 🚨
Implement comprehensive error handling:
class OAuth2Error extends Error {
constructor(message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
app.use((err, req, res, next) => {
if (err instanceof OAuth2Error) {
return res.status(err.statusCode).json({
error: err.message
})
}
console.error(err)
res.status(500).json({
error: 'Internal server error'
})
})
Security Best Practices 🔒
1. PKCE Implementation
Add PKCE (Proof Key for Code Exchange) support:
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, '')
}
2. State Parameter Validation
function generateState() {
return crypto.randomBytes(32).toString('hex')
}
app.get('/auth/login', (req, res, next) => {
const state = generateState()
req.session.oauthState = state
passport.authenticate('oauth2', {
state: state
})(req, res, next)
})
3. Secure 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
}
}))
Testing OAuth2 Implementation
Create test cases using Jest:
describe('OAuth2 Authentication', () => {
test('should redirect to authorization URL', async () => {
const response = await request(app)
.get('/auth/login')
expect(response.status).toBe(302)
expect(response.header.location)
.toMatch(/^https:\/\/provider\.com\/oauth\/authorize/)
})
test('should handle callback with valid code', async () => {
const response = await request(app)
.get('/auth/callback')
.query({
code: 'valid_code',
state: 'valid_state'
})
expect(response.status).toBe(302)
expect(response.header.location).toBe('/')
})
})
Monitoring and Logging
Implement proper logging:
const winston = require('winston')
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({
filename: 'oauth2-error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'oauth2.log'
})
]
})
Conclusion
Implementing OAuth2 in Node.js requires careful attention to security details and best practices. By following this guide, you've learned how to:
- Set up OAuth2 authentication flows
- Manage tokens securely
- Implement security best practices
- Handle errors appropriately
- Test your implementation
For more information, visit the official OAuth2 documentation and the Passport.js documentation.