CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-openid-client

OAuth 2.0 and OpenID Connect client library for JavaScript runtimes with comprehensive authentication flows and security features.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

passport-integration.mddocs/

Passport.js Integration

Complete Passport.js strategy for OpenID Connect authentication with Express.js applications, providing seamless integration with the Passport authentication ecosystem.

Capabilities

Strategy Class

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");
  }
);

Strategy Options

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 });
  }
));

Verify Functions

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
));

Authentication Options

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"
}));

Strategy Method Overrides

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 });
  }
));

Complete Express.js Application Example

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");
  });
});

DPoP Integration Helper

/**
 * 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

docs

advanced-security.md

authorization-flows.md

client-authentication.md

configuration.md

grant-types.md

index.md

passport-integration.md

protected-resources.md

token-management.md

tile.json