Skip to main content

Express OAuth Implementation

Introduction

OAuth (Open Authorization) is an open standard authentication protocol that allows users to grant third-party applications limited access to their resources without sharing their credentials. It's commonly used when you want to let users log into your application using existing accounts from services like Google, Facebook, GitHub, or Twitter.

In this guide, we'll explore how to implement OAuth authentication in an Express.js application. We'll use the popular Passport.js library, which simplifies the OAuth implementation process and provides a consistent API for various authentication strategies.

Prerequisites

Before diving into the implementation, make sure you have:

  • Basic knowledge of Node.js and Express
  • Familiarity with authentication concepts
  • Node.js and npm installed
  • A code editor

Understanding OAuth Flow

The OAuth flow typically involves these steps:

  1. Your application redirects users to the OAuth provider (e.g., Google)
  2. Users authenticate with the provider and grant permissions
  3. The provider redirects back to your application with an authorization code
  4. Your application exchanges this code for an access token
  5. Your application uses the access token to access user information

This flow allows users to authenticate without sharing their login credentials with your application directly.

Setting Up the Project

Let's start by creating a new Express application:

bash
mkdir express-oauth-demo
cd express-oauth-demo
npm init -y
npm install express passport passport-google-oauth20 dotenv express-session

Create the basic Express server structure:

javascript
// app.js
const express = require('express');
const session = require('express-session');
require('dotenv').config();

const app = express();

// Configure session middleware
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));

// Basic route
app.get('/', (req, res) => {
res.send('<h1>Express OAuth Demo</h1><a href="/auth/google">Login with Google</a>');
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Creating OAuth Credentials

Before implementing OAuth, you need to register your application with the OAuth provider. Let's use Google as an example:

  1. Go to the Google Cloud Console
  2. Create a new project
  3. Navigate to "APIs & Services" > "Credentials"
  4. Click "Create Credentials" > "OAuth client ID"
  5. Set the application type to "Web application"
  6. Add authorized redirect URIs (e.g., http://localhost:3000/auth/google/callback)
  7. Take note of your Client ID and Client Secret

Now create a .env file to store these credentials:

GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
SESSION_SECRET=your-random-string

Implementing Google OAuth with Passport

Now let's implement OAuth authentication using Passport.js:

javascript
// app.js
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
require('dotenv').config();

const app = express();

// Configure session middleware
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

// Configure Passport to use Google OAuth
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, (accessToken, refreshToken, profile, done) => {
// In a real application, you would save or find the user in your database
// For this demo, we'll just use the profile object
return done(null, profile);
}));

// Serialize user data for the session
passport.serializeUser((user, done) => {
done(null, user);
});

// Deserialize user from the session
passport.deserializeUser((obj, done) => {
done(null, obj);
});

// Basic route
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.send(`
<h1>Express OAuth Demo</h1>
<p>Logged in as: ${req.user.displayName}</p>
<img src="${req.user.photos[0].value}" width="100">
<br>
<a href="/logout">Logout</a>
`);
} else {
res.send('<h1>Express OAuth Demo</h1><a href="/auth/google">Login with Google</a>');
}
});

// Route to initiate Google OAuth
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);

// Callback route after Google has authenticated the user
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/' }),
(req, res) => {
// Successful authentication, redirect to home page
res.redirect('/');
}
);

// Logout route
app.get('/logout', (req, res) => {
req.logout(function(err) {
if (err) { return next(err); }
res.redirect('/');
});
});

// Middleware to check if user is authenticated
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/');
}

// Protected route example
app.get('/profile', isAuthenticated, (req, res) => {
res.send(`
<h1>Protected Profile Page</h1>
<p>User ID: ${req.user.id}</p>
<p>Name: ${req.user.displayName}</p>
<p>Email: ${req.user.emails?.[0]?.value || 'Not provided'}</p>
<a href="/">Home</a>
`);
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Testing the Implementation

To test the implementation:

  1. Run the application with node app.js
  2. Navigate to http://localhost:3000 in your browser
  3. Click the "Login with Google" link
  4. Authorize your application when prompted by Google
  5. You should be redirected back to your application and see your Google profile information

Adding Multiple OAuth Providers

In real-world applications, you might want to support multiple OAuth providers. Let's extend our example to include GitHub authentication:

First, install the required package:

bash
npm install passport-github2

Then update your application:

javascript
const GitHubStrategy = require('passport-github2').Strategy;

// Add GitHub strategy
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: '/auth/github/callback'
}, (accessToken, refreshToken, profile, done) => {
// In a real application, you would save or find the user in your database
return done(null, profile);
}));

// GitHub authentication routes
app.get('/auth/github',
passport.authenticate('github', { scope: ['user:email'] })
);

app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/' }),
(req, res) => {
// Successful authentication, redirect home
res.redirect('/');
}
);

// Update home route to show both options
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.send(`
<h1>Express OAuth Demo</h1>
<p>Logged in as: ${req.user.displayName || req.user.username}</p>
<img src="${req.user.photos?.[0]?.value || ''}" width="100">
<br>
<a href="/logout">Logout</a>
`);
} else {
res.send(`
<h1>Express OAuth Demo</h1>
<a href="/auth/google">Login with Google</a><br>
<a href="/auth/github">Login with GitHub</a>
`);
}
});

Don't forget to add GitHub credentials to your .env file:

GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret

Storing User Information

In a production application, you'll want to store user information in a database. Here's a simplified example using MongoDB with Mongoose:

bash
npm install mongoose
javascript
const mongoose = require('mongoose');

// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/oauth-demo');

// Define User model
const userSchema = new mongoose.Schema({
oauthId: String,
provider: String,
displayName: String,
email: String,
photo: String,
createdAt: {
type: Date,
default: Date.now
}
});

const User = mongoose.model('User', userSchema);

// Update 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({ oauthId: profile.id, provider: 'google' });

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

return done(null, user);
} catch (err) {
return done(err, null);
}
}));

// Update serialization to use database ID
passport.serializeUser((user, done) => {
done(null, user._id);
});

passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err, null);
}
});

Security Considerations

When implementing OAuth, keep these security considerations in mind:

  1. Always use HTTPS in production
  2. Keep credentials secure and never commit them to version control
  3. Validate state parameters to prevent CSRF attacks
  4. Implement proper session handling and security
  5. Restrict callback URLs to only trusted domains
  6. Regularly rotate and update secrets
  7. Request minimal scopes needed for your application

Best Practices

  • Use a dedicated library like Passport.js rather than implementing OAuth from scratch
  • Store tokens securely and encrypt sensitive information
  • Implement proper error handling for authentication failures
  • Use environment variables for configuration
  • Implement proper logging for security events
  • Consider token refreshing strategies for long-lived applications

Practical Example: Social Media Dashboard

Here's a real-world example of how you might use OAuth in a social media dashboard application:

javascript
// routes/dashboard.js
const express = require('express');
const router = express.Router();
const axios = require('axios');

// Middleware to check if user is authenticated
function ensureAuth(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/');
}

// Dashboard home - protected route
router.get('/', ensureAuth, (req, res) => {
res.render('dashboard', {
user: req.user,
title: 'Social Dashboard'
});
});

// Fetch Twitter timeline using stored OAuth token
router.get('/twitter-feed', ensureAuth, async (req, res) => {
try {
// In a real app, you'd get this from the user's stored tokens
const accessToken = req.user.twitter.token;

const response = await axios.get('https://api.twitter.com/1.1/statuses/home_timeline.json', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});

res.json(response.data);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch Twitter feed' });
}
});

module.exports = router;

This example shows how OAuth tokens can be used to access API resources on behalf of the user after authentication.

Summary

In this guide, we've explored how to implement OAuth authentication in Express applications:

  1. We set up an Express application with Passport.js
  2. We integrated Google OAuth authentication
  3. We extended the application to support multiple OAuth providers
  4. We discussed storing user information in a database
  5. We covered security considerations and best practices
  6. We looked at a practical example of using OAuth tokens in a real application

OAuth provides a secure way to authenticate users via trusted third parties without handling sensitive credentials yourself. By leveraging OAuth providers, you can enhance security, simplify the user experience, and gain access to rich user data from trusted sources.

Additional Resources

Exercises

  1. Add Facebook OAuth authentication to the example application
  2. Implement a user profile page that displays data from multiple connected accounts
  3. Create a system that merges user accounts if the same email is used across different OAuth providers
  4. Build a simple permissions system based on OAuth scopes
  5. Add token refreshing functionality to maintain long-term API access

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)