CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-passport

Simple, unobtrusive authentication for Node.js

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

strategy-development.mddocs/

Strategy Development

Framework for creating custom authentication strategies and understanding the strategy interface for advanced authentication scenarios. Strategies define how authentication is performed and can be customized for any authentication mechanism.

Capabilities

Base Strategy Class

All Passport strategies must inherit from the base Strategy class provided by the passport-strategy package.

/**
 * Base strategy class from passport-strategy package
 * All custom strategies must inherit from this class
 */
const Strategy = require("passport-strategy");

class CustomStrategy extends Strategy {
  /**
   * Create a new strategy instance
   * @param {Object} [options] - Strategy configuration options
   * @param {Function} verify - Verification callback function
   */
  constructor(options, verify) {
    super();
    this.name = "strategy-name"; // Required: unique strategy name
    // Additional initialization
  }
  
  /**
   * Main authentication method - must be implemented
   * @param {http.IncomingMessage} req - HTTP request object
   * @param {Object} [options] - Authentication options
   */
  authenticate(req, options) {
    // Authentication logic implementation
  }
}

Strategy Interface

Strategies have access to several action methods for communicating authentication results:

/**
 * Signal successful authentication
 * @param {Object} user - Authenticated user object
 * @param {Object} [info] - Additional authentication info
 */
this.success(user, info);

/**
 * Signal authentication failure
 * @param {string} [challenge] - Authentication challenge message
 * @param {number} [status=401] - HTTP status code
 */
this.fail(challenge, status);

/**
 * Redirect to external authentication provider
 * @param {string} url - Redirect URL
 * @param {number} [status=302] - HTTP status code
 */
this.redirect(url, status);

/**
 * Pass without making success/failure decision
 * Used for session restoration and optional authentication
 */
this.pass();

/**
 * Signal internal authentication error
 * @param {Error} err - Error object
 */
this.error(err);

Strategy Action Method Details:

The strategy action methods are dynamically attached during authentication and handle complex logic including flash messages, redirects, session management, and error handling:

/**
 * Success method handles authentication success with extensive option processing
 * - Processes successFlash, successMessage, and successRedirect options
 * - Handles user property assignment via assignProperty option
 * - Manages session login via req.logIn() when session enabled
 * - Transforms auth info via passport.transformAuthInfo() when authInfo enabled
 */
strategy.success = function(user, info) {
  // Internal implementation handles:
  // - Flash message processing
  // - Session messages
  // - Property assignment
  // - Session login
  // - Auth info transformation
  // - Redirect handling
};

/**
 * Fail method handles authentication failure with challenge accumulation
 * - Accumulates challenges from multiple strategies
 * - Processes failureFlash and failureMessage options
 * - Sets WWW-Authenticate header with challenges
 * - Handles failureRedirect or failWithError options
 */
strategy.fail = function(challenge, status) {
  // Internal implementation handles:
  // - Challenge accumulation across strategies
  // - Flash message processing
  // - HTTP header setting
  // - Error vs redirect handling
};

/**
 * Redirect method handles external redirects (OAuth flows)
 * - Sets appropriate HTTP status code (default 302)
 * - Performs redirect to external authentication provider
 */
strategy.redirect = function(url, status) {
  // Internal implementation performs HTTP redirect
};

/**
 * Pass method allows strategy to defer decision
 * - Used for optional authentication scenarios
 * - Continues middleware chain without success/failure
 * - Common in session strategies when no session data exists
 */
strategy.pass = function() {
  // Internal implementation continues to next middleware
};

/**
 * Error method handles internal authentication errors
 * - Bypasses normal authentication flow
 * - Passes error to Express error handling middleware
 */
strategy.error = function(err) {
  // Internal implementation calls next(err)
};

Custom Strategy Example

Here's a complete example of a custom API key authentication strategy:

const Strategy = require("passport-strategy");
const util = require("util");

/**
 * Custom API Key authentication strategy
 * @param {Object} options - Strategy options
 * @param {string} [options.apiKeyHeader='x-api-key'] - Header name for API key
 * @param {Function} verify - Verification function (apiKey, done) => void
 */
function ApiKeyStrategy(options, verify) {
  if (typeof options === "function") {
    verify = options;
    options = {};
  }
  if (!verify) {
    throw new TypeError("ApiKeyStrategy requires a verify callback");
  }
  
  Strategy.call(this);
  
  this.name = "apikey";
  this._verify = verify;
  this._apiKeyHeader = options.apiKeyHeader || "x-api-key";
}

// Inherit from Strategy
util.inherits(ApiKeyStrategy, Strategy);

/**
 * Authenticate request using API key
 * @param {http.IncomingMessage} req - HTTP request
 * @param {Object} [options] - Authentication options
 */
ApiKeyStrategy.prototype.authenticate = function(req, options) {
  const apiKey = req.headers[this._apiKeyHeader];
  
  if (!apiKey) {
    return this.fail("Missing API key", 401);
  }
  
  const self = this;
  
  function verified(err, user, info) {
    if (err) return self.error(err);
    if (!user) return self.fail(info || "Invalid API key", 401);
    return self.success(user, info);
  }
  
  try {
    this._verify(apiKey, verified);
  } catch (ex) {
    return this.error(ex);
  }
};

module.exports = ApiKeyStrategy;

Usage of Custom Strategy:

const ApiKeyStrategy = require("./apikey-strategy");

// Register the custom strategy
passport.use(new ApiKeyStrategy(
  { apiKeyHeader: "authorization" },
  async (apiKey, done) => {
    try {
      const user = await User.findOne({ apiKey: apiKey });
      if (!user) {
        return done(null, false, { message: "Invalid API key" });
      }
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// Use the strategy
app.get("/api/data",
  passport.authenticate("apikey", { session: false }),
  (req, res) => {
    res.json({ data: "protected data", user: req.user.id });
  }
);

OAuth Strategy Pattern

OAuth strategies typically involve redirection to external providers:

const Strategy = require("passport-strategy");
const OAuth2 = require("oauth").OAuth2;

/**
 * Custom OAuth2 strategy example
 * @param {Object} options - OAuth2 configuration
 * @param {Function} verify - Verification callback
 */
function CustomOAuth2Strategy(options, verify) {
  Strategy.call(this);
  
  this.name = "custom-oauth2";
  this._verify = verify;
  this._oauth2 = new OAuth2(
    options.clientID,
    options.clientSecret,
    options.authorizationURL,
    options.tokenURL
  );
  this._callbackURL = options.callbackURL;
}

util.inherits(CustomOAuth2Strategy, Strategy);

CustomOAuth2Strategy.prototype.authenticate = function(req, options) {
  if (req.query && req.query.code) {
    // Handle callback from OAuth provider
    this._oauth2.getOAuthAccessToken(
      req.query.code,
      { grant_type: "authorization_code" },
      (err, accessToken, refreshToken, results) => {
        if (err) return this.error(err);
        
        // Get user profile with access token
        this._oauth2.get(
          "https://api.provider.com/user",
          accessToken,
          (err, body) => {
            if (err) return this.error(err);
            
            const profile = JSON.parse(body);
            this._verify(accessToken, refreshToken, profile, (err, user) => {
              if (err) return this.error(err);
              if (!user) return this.fail();
              return this.success(user);
            });
          }
        );
      }
    );
  } else {
    // Redirect to OAuth provider
    const authURL = this._oauth2.getAuthorizeUrl({
      response_type: "code",
      client_id: this._oauth2._clientId,
      redirect_uri: this._callbackURL,
      scope: options.scope || "read"
    });
    this.redirect(authURL);
  }
};

Built-in Session Strategy

Understanding the built-in SessionStrategy implementation:

/**
 * Built-in session strategy for restoring authentication from session
 * @param {Object|Function} [options] - Strategy options or deserializeUser function if only one parameter
 * @param {string} [options.key='passport'] - Session key
 * @param {Function} deserializeUser - User deserialization function
 */
class SessionStrategy extends Strategy {
  constructor(options, deserializeUser);
  constructor(deserializeUser);
  name: "session";
  
  /**
   * Authenticate based on session data
   * @param {http.IncomingMessage} req - HTTP request
   * @param {Object} [options] - Authentication options
   * @param {boolean} [options.pauseStream=false] - Pause request stream
   */
  authenticate(req, options);
}

Strategy Testing

Example of how to test custom strategies:

const chai = require("chai");
const expect = chai.expect;
const ApiKeyStrategy = require("../lib/apikey-strategy");

describe("ApiKeyStrategy", () => {
  let strategy;
  
  beforeEach(() => {
    strategy = new ApiKeyStrategy((apiKey, done) => {
      if (apiKey === "valid-key") {
        return done(null, { id: 1, name: "Test User" });
      }
      return done(null, false);
    });
  });
  
  it("should authenticate with valid API key", (done) => {
    const req = {
      headers: { "x-api-key": "valid-key" }
    };
    
    strategy.success = (user) => {
      expect(user).to.deep.equal({ id: 1, name: "Test User" });
      done();
    };
    
    strategy.authenticate(req);
  });
  
  it("should fail with invalid API key", (done) => {
    const req = {
      headers: { "x-api-key": "invalid-key" }
    };
    
    strategy.fail = (challenge, status) => {
      expect(challenge).to.equal("Invalid API key");
      expect(status).to.equal(401);
      done();
    };
    
    strategy.authenticate(req);
  });
  
  it("should fail with missing API key", (done) => {
    const req = { headers: {} };
    
    strategy.fail = (challenge, status) => {
      expect(challenge).to.equal("Missing API key");
      expect(status).to.equal(401);
      done();
    };
    
    strategy.authenticate(req);
  });
});

Advanced Strategy Patterns

Multi-step Authentication

Strategies can implement multi-step authentication flows:

class TwoFactorStrategy extends Strategy {
  constructor(options, verify) {
    super();
    this.name = "twofactor";
    this._verify = verify;
  }
  
  authenticate(req, options) {
    const { username, password, token } = req.body;
    
    if (!username || !password) {
      return this.fail("Missing credentials", 400);
    }
    
    // First step: verify username/password
    this._verify(username, password, (err, user) => {
      if (err) return this.error(err);
      if (!user) return this.fail("Invalid credentials", 401);
      
      if (!token) {
        // Request second factor
        return this.fail("Two-factor token required", 200);
      }
      
      // Second step: verify 2FA token
      if (this.verifyToken(user, token)) {
        return this.success(user);
      } else {
        return this.fail("Invalid token", 401);
      }
    });
  }
  
  verifyToken(user, token) {
    // Implement 2FA token verification
    return user.twoFactorSecret && 
           require("speakeasy").totp.verify({
             secret: user.twoFactorSecret,
             encoding: "base32",
             token: token
           });
  }
}

Conditional Authentication

Strategies can implement conditional logic:

class SmartStrategy extends Strategy {
  constructor(options, verify) {
    super();
    this.name = "smart";
    this._verify = verify;
  }
  
  authenticate(req, options) {
    const ipAddress = req.ip;
    const userAgent = req.headers["user-agent"];
    
    // Check if this is a trusted environment
    if (this.isTrustedEnvironment(ipAddress, userAgent)) {
      // Skip authentication for trusted environments
      return this.pass();
    }
    
    // Require authentication for untrusted environments
    const token = this.extractToken(req);
    if (!token) {
      return this.fail("Authentication required", 401);
    }
    
    this._verify(token, (err, user) => {
      if (err) return this.error(err);
      if (!user) return this.fail("Invalid token", 401);
      return this.success(user);
    });
  }
  
  isTrustedEnvironment(ip, userAgent) {
    // Implement trust logic
    return ip.startsWith("192.168.") || 
           userAgent.includes("TrustedApp");
  }
  
  extractToken(req) {
    const authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith("Bearer ")) {
      return authHeader.substring(7);
    }
    return null;
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-passport

docs

core-authentication.md

index.md

session-management.md

strategy-development.md

tile.json