Third-Party Authentication with OAuth
The Valet Key Analogy
Imagine your application is a guest at a fancy hotel (like “Innovate Inc.”). Your guest wants to use the hotel’s secure vault (their GitHub or Google account data) without giving you, a stranger, the master key to their room.
This is where OAuth comes in. Instead of asking for the user’s password (the master key), your application asks the user to go to the hotel’s front desk (the provider, e.g., Google). The user proves their identity to the front desk, and tells the clerk, “It’s okay to let this application access my vault for a little while to grab my profile picture.”
The front desk then gives your application a temporary, limited-access valet key (an OAuth access token). This key can only open specific doors (e.g., read basic profile data) and expires after a short time. It can’t be used to change the user’s password or delete their account. This is the core principle of delegated authorization.
Understanding the OAuth 2.0 Flow
OAuth 2.0 is the industry-standard protocol for authorization. It specifies a process that allows a user to grant a third-party application access to their resources on another service, without sharing their credentials.
You have likely used OAuth 2.0 without knowing it. For example, when you log in to a website with your Google account, GitHub account, or Apple account, etc., you are using OAuth 2.0.
The flow involves four main parties:
- Resource Owner: The user who owns the data (e.g., you, with your Google account).
- Client: The application requesting access (e.g., the “Innovate Inc.” portal).
- Authorization Server: The service that owns the user’s identity and issues the access tokens (e.g., Google’s or GitHub’s authentication servers).
- Resource Server: The server that hosts the user’s data (e.g., the Google or GitHub API).
The process, known as the Authorization Code Grant Flow, works like this:
- Authorization Request: The user clicks “Login with Google.” The Client (our app) redirects the user to the Authorization Server (Google), sending along its
Client ID
and requestedscopes
(permissions). - User Grants Consent: The Authorization Server asks the user to log in and approve the requested permissions for the Client.
- Authorization Code: If the user approves, the Authorization Server redirects them back to the Client with a temporary
authorization code
. - Request Access Token: The Client sends this
authorization code
along with itsClient ID
andClient Secret
back to the Authorization Server. This happens on the backend, hidden from the user’s browser. - Issue Access Token: The Authorization Server verifies the code and credentials, and if they match, issues a short-lived
access token
back to the Client. - Access Resources: The Client can now use this
access token
to make requests to the Resource Server (e.g., the Google API) to fetch the user’s profile information.
Integrating an OAuth Strategy with Passport.js
Passport.js makes this complex flow manageable by using “strategies.” For each provider (Google, GitHub, Facebook, etc.), there is a corresponding Passport strategy package. We’ll use passport-github2
as our example.
1. Registering Your Application
First, you must register your application on the provider’s developer portal. For GitHub:
- Go to Settings -> Developer settings -> OAuth Apps -> New OAuth App.
- Fill in the application details.
- The most important field is the Authorization callback URL. This is the URL in your application where the provider will redirect the user after they grant consent. For local development, this is often
http://localhost:3001/api/users/auth/github/callback
. - Once registered, GitHub will provide a Client ID and a Client Secret. Treat the secret like a password and store it in your
.env
file.
2. Configuring the Passport Strategy
Install the necessary package: npm install passport passport-github2
.
Now, configure Passport to use the strategy. This code would typically live in a passport.js
configuration file that is loaded by your server.
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;
const User = require('../models/User');
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL, // e.g., 'http://localhost:3001/api/users/auth/github/callback'
},
// This is the "verify" callback
async (accessToken, refreshToken, profile, done) => {
try {
// The "profile" object contains the user's GitHub information
const existingUser = await User.findOne({ githubId: profile.id });
if (existingUser) {
// If user already exists, pass them to the next middleware
return done(null, existingUser);
}
// If it's a new user, create a record in our database
const newUser = new User({
githubId: profile.id,
username: profile.username,
email: profile.emails[0].value, // Some providers return an array of emails
});
await newUser.save();
done(null, newUser);
} catch (err) {
done(err);
}
}
)
);
// These functions are needed for session management
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => done(err, user));
});
The verify
callback is the core of the strategy. It receives the accessToken
and the user’s profile
from GitHub. Your job is to either find an existing user in your database with that profile.id
or create a new user record.
3. Setting Up the Authentication Routes
You need two routes: one to initiate the process, and one for the callback.
// ... other routes
// Route to start the OAuth flow
// When a user visits this URL, they will be redirected to GitHub to log in.
router.get(
'/auth/github',
passport.authenticate('github', { scope: ['user:email'] }) // Request email scope
);
// The callback route that GitHub will redirect to after the user approves.
router.get(
'/auth/github/callback',
passport.authenticate('github', {
failureRedirect: '/login', // Where to redirect if user denies
session: false // We are using tokens, not sessions
}),
(req, res) => {
// At this point, `req.user` is the user profile returned from the verify callback.
// We can now issue our own JWT to the user.
const token = signToken(req.user);
// Redirect the user to the frontend with the token, or send it in the response
res.redirect(`http://localhost:3000?token=${token}`);
}
);
Knowledge Check
In the OAuth 2.0 flow, what is the primary purpose of the 'authorization code'?
- Select an answer to view feedback.
Why is it critical for the client to exchange the authorization code for an access token via a secure backend request, rather than in the user's browser?
- Select an answer to view feedback.
Summary
In this lesson, you explored third-party authentication using the OAuth 2.0 protocol. You learned how the “valet key” analogy explains delegated access and how the Authorization Code Grant Flow securely allows a user to grant your application access without sharing their password. We walked through the practical steps of using Passport.js to integrate OAuth, including registering an app with a provider (GitHub), configuring a Passport strategy, and setting up the necessary routes to handle the authentication and callback process.