Single Sign-On (SSO) Implementation Using Passport.js in TypeScript

Table of contents

Implementing Single Sign-On (SSO) in your application enhances user experience by allowing seamless access across multiple platforms with a single set of credentials. In Node.js applications, the Passport middleware simplifies this process by providing a flexible and modular authentication framework. This guide focuses on using Passport.js with TypeScript to maintain type safety and robust application architecture.
What Is Passport.js?
Passport.js is an authentication middleware for Node.js that supports various strategies, including social logins (e.g., Google, GitHub) and traditional username-password mechanisms. It is modular, allowing developers to choose specific strategies as needed.
For more details, visit Passport.js documentation.
Steps to Implement SSO in TypeScript
Install Required Dependencies
Begin by installing the necessary packages:
npm install passport passport-google-oauth20 passport-github2 express-session @types/passport @types/express-session
Initialize Passport
Configure Passport and session handling in your application.
Code Implementation:
import express, { Application, Request, Response, NextFunction } from "express";
import session from "express-session";
import passport from "passport";
const app: Application = express();
app.use(
session({
secret: "your_secret_key",
resave: false,
saveUninitialized: true,
})
);
app.use(passport.initialize());
app.use(passport.session());
Configure Authentication Strategies
Google Strategy
To authenticate users via Google, configure the Google strategy:
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "http://localhost:3000/auth/google/callback",
},
async (
accessToken: string,
refreshToken: string,
profile: any,
done: Function
) => {
try {
// Replace with your DB logic
const user = await findOrCreateUser({ googleId: profile.id });
return done(null, user);
} catch (error) {
return done(error);
}
}
)
);
GitHub Strategy
Similarly, configure the GitHub strategy:
import { Strategy as GitHubStrategy } from "passport-github2";
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
callbackURL: "http://localhost:3000/auth/github/callback",
},
async (
accessToken: string,
refreshToken: string,
profile: any,
done: Function
) => {
try {
// Replace with your DB logic
const user = await findOrCreateUser({ githubId: profile.id });
return done(null, user);
} catch (error) {
return done(error);
}
}
)
);
Local Strategy
For applications that require traditional username and password authentication, Passport.js provides the Local Strategy:
import { Strategy as LocalStrategy } from "passport-local";
passport.use(
new LocalStrategy((username, password, done) => {
// Replace with your user retrieval and password verification logic
const user = getUserByUsername(username);
if (!user || !verifyPassword(user, password)) {
return done(null, false, { message: "Invalid credentials" });
}
return done(null, user);
})
);
By utilizing the Local Strategy, you can manage authentication within your application without relying on external providers. This approach is beneficial when you prefer to handle user credentials directly.
Serialization and Deserialization
Handle session serialization and deserialization to maintain user sessions.
Code Implementation:
passport.serializeUser((user: any, done: Function) => {
done(null, user.id);
});
passport.deserializeUser(async (id: string, done: Function) => {
try {
const user = await findUserById(id); // Replace with your DB logic
done(null, user);
} catch (error) {
done(error);
}
});
Define Routes
Set up routes for initiating authentication and handling callbacks.
Google Authentication Routes:
app.get(
"/auth/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/" }),
(req: Request, res: Response) => {
res.redirect("/dashboard");
}
);
GitHub Authentication Routes:
app.get(
"/auth/github",
passport.authenticate("github", { scope: ["user:email"] })
);
app.get(
"/auth/github/callback",
passport.authenticate("github", { failureRedirect: "/" }),
(req: Request, res: Response) => {
res.redirect("/dashboard");
}
);
Local Authentication Routes:
app.post(
"/login",
passport.authenticate("local", { failureRedirect: "/" }),
(req: Request, res: Response) => {
res.redirect("/dashboard");
}
);
Handle User Logout
Allow users to log out and securely end their sessions.
Code Implementation:
app.get("/logout", (req: Request, res: Response, next: NextFunction) => {
req.logout((err) => {
if (err) {
return next(err);
}
res.redirect("/");
});
});
Handling User Authentication
Protect routes that require authentication by checking if the user is authenticated, implementing a middleware function.
Code Implementation:
function isAuthenticated(req: Request, res: Response, next: NextFunction) {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/");
}
Now, you can use the isAuthenticated
middleware to protect routes that require authentication:
app.get("/dashboard", isAuthenticated, (req: Request, res: Response) => {
res.send("Dashboard", { user: req.user });
});
Environment Variables
Store sensitive credentials in an .env
file:
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
Use a library like dotenv
to load them:
import dotenv from "dotenv";
dotenv.config();
Replace Placeholder Functions
Replace the placeholder functions findOrCreateUser
and findUserById
with actual implementations for interacting with your database:
async function findOrCreateUser(query: object): Promise<any> {
// Example: Interact with database to find or create user
return {};
}
async function findUserById(id: string): Promise<any> {
// Example: Fetch user by ID from database
return {};
}
Conclusion
By leveraging Passport.js in TypeScript, you can create a robust SSO system with type safety, flexibility, and support for multiple authentication providers like Google and GitHub. For additional configurations, refer to the Passport.js documentation.
Implementing SSO not only enhances user convenience but also strengthens security by centralizing authentication processes.
Subscribe to my newsletter
Read articles from Kshitiz Acharya directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Kshitiz Acharya
Kshitiz Acharya
Into tech!