OAuth 2.0 and OpenID Connect client library for JavaScript runtimes with comprehensive authentication flows and security features.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Complete Passport.js strategy for OpenID Connect authentication with Express.js applications, providing seamless integration with the Passport authentication ecosystem.
Passport.js strategy implementation for OpenID Connect authentication.
/**
* OpenID Connect Passport strategy
*/
class Strategy implements passport.Strategy {
/** Strategy name */
readonly name: string;
constructor(options: StrategyOptions, verify: VerifyFunction);
constructor(options: StrategyOptionsWithRequest, verify: VerifyFunctionWithRequest);
authenticate<TOptions extends AuthenticateOptions>(
req: express.Request,
options: TOptions
): void;
}Usage Examples:
import * as client from "openid-client";
import { Strategy } from "openid-client/passport";
import passport from "passport";
import express from "express";
const app = express();
// Configure OpenID Connect client
const config = await client.discovery(
new URL("https://accounts.google.com"),
process.env.CLIENT_ID!,
process.env.CLIENT_SECRET!
);
// Configure Passport strategy
passport.use(
"oidc",
new Strategy(
{
config: config,
callbackURL: "https://example.com/auth/callback",
scope: "openid email profile"
},
(tokens, done) => {
// Verify callback - process tokens
const claims = tokens.claims();
const user = {
id: claims?.sub,
email: claims?.email,
name: claims?.name,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token
};
return done(null, user);
}
)
);
// Routes
app.get("/auth/login", passport.authenticate("oidc"));
app.get("/auth/callback",
passport.authenticate("oidc", { failureRedirect: "/login" }),
(req, res) => {
res.redirect("/dashboard");
}
);Configuration options for the Passport strategy.
interface StrategyOptions extends StrategyOptionsBase {
/** Do not pass request to verify callback */
passReqToCallback?: false;
}
interface StrategyOptionsWithRequest extends StrategyOptionsBase {
/** Pass request as first argument to verify callback */
passReqToCallback: true;
}
interface StrategyOptionsBase {
/** OpenID Client configuration instance */
config: Configuration;
/** Strategy name (defaults to issuer hostname) */
name?: string;
/** Session key for storing state (defaults to issuer hostname) */
sessionKey?: string;
/** DPoP handle retrieval function */
DPoP?: getDPoPHandle;
/** Callback URL */
callbackURL?: URL | string;
/** OAuth 2.0 scope */
scope?: string;
/** Rich Authorization Requests */
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[];
/** Resource indicators */
resource?: string | string[];
/** Use PAR (Pushed Authorization Requests) */
usePAR?: boolean;
/** Use JAR (JWT Authorization Requests) */
useJAR?: false | CryptoKey | PrivateKey | [CryptoKey | PrivateKey, ModifyAssertionFunction];
}Advanced Configuration Examples:
import * as client from "openid-client";
import { Strategy } from "openid-client/passport";
// Strategy with PAR (Pushed Authorization Requests)
passport.use(new Strategy(
{
config: config,
callbackURL: "https://example.com/auth/callback",
scope: "openid email profile",
usePAR: true // Use PAR for enhanced security
},
(tokens, done) => {
// Process tokens
return done(null, { user: tokens.claims()?.sub });
}
));
// Strategy with JAR (JWT Authorization Requests)
const jarKeyPair = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign"]
);
passport.use(new Strategy(
{
config: config,
callbackURL: "https://example.com/auth/callback",
scope: "openid email profile",
useJAR: jarKeyPair.privateKey // Sign authorization requests
},
(tokens, done) => {
return done(null, { user: tokens.claims()?.sub });
}
));
// Strategy with DPoP support
passport.use(new Strategy(
{
config: config,
callbackURL: "https://example.com/auth/callback",
scope: "openid email profile",
DPoP: async (req) => {
// Return DPoP handle based on request/session
const dpopKeyPair = await getDPoPKeyPairFromSession(req);
if (dpopKeyPair) {
return client.getDPoPHandle(config, dpopKeyPair);
}
return undefined;
}
},
(tokens, done) => {
return done(null, { user: tokens.claims()?.sub });
}
));Callback functions for processing authentication results.
/**
* Standard verify function
* @param tokens - Token response from authorization server with helpers
* @param verified - Passport verification callback
*/
type VerifyFunction = (
tokens: TokenEndpointResponse & TokenEndpointResponseHelpers,
verified: passport.AuthenticateCallback
) => void;
/**
* Verify function with request object
* @param req - Express request object
* @param tokens - Token response from authorization server with helpers
* @param verified - Passport verification callback
*/
type VerifyFunctionWithRequest = (
req: express.Request,
tokens: TokenEndpointResponse & TokenEndpointResponseHelpers,
verified: passport.AuthenticateCallback
) => void;Verify Function Examples:
import * as client from "openid-client";
import { Strategy } from "openid-client/passport";
// Standard verify function
const verifyUser: VerifyFunction = (tokens, done) => {
const claims = tokens.claims();
if (!claims) {
return done(new Error("No ID token claims"));
}
// Find or create user in database
User.findOrCreate({
where: { sub: claims.sub },
defaults: {
email: claims.email,
name: claims.name,
picture: claims.picture
}
}).then(([user, created]) => {
// Store tokens for API access
user.accessToken = tokens.access_token;
user.refreshToken = tokens.refresh_token;
return done(null, user);
}).catch(err => {
return done(err);
});
};
// Verify function with request access
const verifyUserWithRequest: VerifyFunctionWithRequest = (req, tokens, done) => {
const claims = tokens.claims();
// Access request information
const userAgent = req.get("User-Agent");
const ipAddress = req.ip;
// Log authentication
console.log(`User ${claims?.sub} authenticated from ${ipAddress}`);
// Store additional session information
req.session.tokenExpiresAt = Date.now() + (tokens.expires_in || 3600) * 1000;
return done(null, {
id: claims?.sub,
email: claims?.email,
name: claims?.name,
lastLogin: new Date(),
userAgent,
ipAddress
});
};
// Use with strategy
passport.use(new Strategy(
{
config: config,
callbackURL: "https://example.com/auth/callback",
passReqToCallback: true
},
verifyUserWithRequest
));Options for authentication requests.
interface AuthenticateOptions extends passport.AuthenticateOptions {
/** Resource indicators (request-specific) */
resource?: string | string[];
/** Login hint for authorization request */
loginHint?: string;
/** ID Token hint for authorization request */
idTokenHint?: string;
/** Rich Authorization Requests (request-specific) */
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[];
/** OpenID Connect prompt parameter */
prompt?: string;
/** OAuth 2.0 scope (request-specific) */
scope?: string | string[];
/** Callback URL (request-specific) */
callbackURL?: URL | string;
}Authentication Examples:
import express from "express";
import passport from "passport";
const app = express();
// Basic authentication
app.get("/auth/login", passport.authenticate("oidc"));
// Authentication with specific scope
app.get("/auth/admin", passport.authenticate("oidc", {
scope: "openid email profile admin:read admin:write"
}));
// Authentication with resource indicator
app.get("/auth/api", passport.authenticate("oidc", {
resource: "https://api.example.com"
}));
// Authentication with login hint
app.get("/auth/user/:email", (req, res, next) => {
passport.authenticate("oidc", {
loginHint: req.params.email
})(req, res, next);
});
// Force re-authentication
app.get("/auth/reauth", passport.authenticate("oidc", {
prompt: "login"
}));
// Request consent
app.get("/auth/consent", passport.authenticate("oidc", {
prompt: "consent"
}));Extensible methods for customizing strategy behavior.
/**
* Return additional authorization request parameters
* @param req - Express request object
* @param options - Authentication options
* @returns Additional parameters or undefined
*/
authorizationRequestParams<TOptions extends AuthenticateOptions>(
req: express.Request,
options: TOptions
): URLSearchParams | Record<string, string> | undefined;
/**
* Return additional token endpoint parameters
* @param req - Express request object
* @param options - Authentication options
* @returns Additional parameters or undefined
*/
authorizationCodeGrantParameters<TOptions extends AuthenticateOptions>(
req: express.Request,
options: TOptions
): URLSearchParams | Record<string, string> | undefined;
/**
* Determine current request URL
* @param req - Express request object
* @returns Current URL for callback processing
*/
currentUrl(req: express.Request): URL;
/**
* Determine whether to initiate authorization request
* @param req - Express request object
* @param currentUrl - Current request URL
* @param options - Authentication options
* @returns True to initiate authorization, false to process callback
*/
shouldInitiateAuthRequest<TOptions extends AuthenticateOptions>(
req: express.Request,
currentUrl: URL,
options: TOptions
): boolean;Custom Strategy Example:
import * as client from "openid-client";
import { Strategy } from "openid-client/passport";
class CustomOIDCStrategy extends Strategy {
// Add custom authorization parameters
authorizationRequestParams(req: express.Request, options: any) {
const params: Record<string, string> = {};
// Add custom parameters based on request
if (req.query.locale) {
params.ui_locales = req.query.locale as string;
}
if (req.headers["x-tenant-id"]) {
params.tenant_hint = req.headers["x-tenant-id"] as string;
}
return params;
}
// Add custom token endpoint parameters
authorizationCodeGrantParameters(req: express.Request, options: any) {
const params: Record<string, string> = {};
// Add tenant information to token request
if (req.headers["x-tenant-id"]) {
params.tenant_id = req.headers["x-tenant-id"] as string;
}
return params;
}
// Custom URL handling for proxies
currentUrl(req: express.Request): URL {
// Handle proxy headers
const protocol = req.headers["x-forwarded-proto"] || req.protocol;
const host = req.headers["x-forwarded-host"] || req.get("host");
const url = req.originalUrl || req.url;
return new URL(`${protocol}://${host}${url}`);
}
// Custom logic for determining auth vs callback
shouldInitiateAuthRequest(req: express.Request, currentUrl: URL, options: any): boolean {
// Custom logic - check for specific callback indicator
return !currentUrl.searchParams.has("code") &&
!currentUrl.searchParams.has("error") &&
req.method === "GET";
}
}
// Use custom strategy
passport.use("custom-oidc", new CustomOIDCStrategy(
{
config: config,
callbackURL: "https://example.com/auth/callback"
},
(tokens, done) => {
return done(null, { user: tokens.claims()?.sub });
}
));import express from "express";
import session from "express-session";
import passport from "passport";
import * as client from "openid-client";
import { Strategy } from "openid-client/passport";
const app = express();
// Session configuration
app.use(session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === "production" }
}));
// Passport middleware
app.use(passport.initialize());
app.use(passport.session());
// Serialize/deserialize user
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id: string, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error);
}
});
// Configure OpenID Connect
async function setupAuth() {
const config = await client.discovery(
new URL(process.env.OIDC_ISSUER!),
process.env.CLIENT_ID!,
process.env.CLIENT_SECRET!
);
passport.use("oidc", new Strategy(
{
config: config,
callbackURL: process.env.CALLBACK_URL!,
scope: "openid email profile"
},
async (tokens, done) => {
try {
const claims = tokens.claims();
const [user] = await User.findOrCreate({
where: { sub: claims?.sub },
defaults: {
email: claims?.email,
name: claims?.name,
picture: claims?.picture
}
});
// Store tokens for API access
await user.update({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
tokenExpiresAt: new Date(Date.now() + (tokens.expires_in || 3600) * 1000)
});
return done(null, user);
} catch (error) {
return done(error);
}
}
));
}
// Authentication routes
app.get("/login", passport.authenticate("oidc"));
app.get("/auth/callback",
passport.authenticate("oidc", { failureRedirect: "/login?error=1" }),
(req, res) => {
res.redirect("/dashboard");
}
);
app.get("/logout", (req, res) => {
req.logout(() => {
res.redirect("/");
});
});
// Protected routes
function requireAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/login");
}
app.get("/dashboard", requireAuth, (req, res) => {
res.json({ user: req.user });
});
// Start server
setupAuth().then(() => {
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
});/**
* Function to retrieve DPoP handle for requests
* @param req - Express request object
* @returns DPoP handle or undefined
*/
type getDPoPHandle = (
req: express.Request
) => Promise<DPoPHandle | undefined> | DPoPHandle | undefined;DPoP Session Management:
import * as client from "openid-client";
// Store DPoP key pair in session
const dpopHelper: getDPoPHandle = async (req) => {
if (!req.session.dpopKeyPair) {
// Generate new key pair for session
req.session.dpopKeyPair = await client.randomDPoPKeyPair();
}
return client.getDPoPHandle(config, req.session.dpopKeyPair);
};
passport.use(new Strategy(
{
config: config,
callbackURL: "https://example.com/auth/callback",
DPoP: dpopHelper
},
(tokens, done) => {
// Tokens will be sender-constrained if server supports DPoP
return done(null, { user: tokens.claims()?.sub });
}
));Install with Tessl CLI
npx tessl i tessl/npm-openid-client