Simple, unobtrusive authentication for Node.js
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.
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
}
}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)
};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 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);
}
};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);
}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);
});
});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
});
}
}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