0
# Strategy Development
1
2
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.
3
4
## Capabilities
5
6
### Base Strategy Class
7
8
All Passport strategies must inherit from the base Strategy class provided by the `passport-strategy` package.
9
10
```javascript { .api }
11
/**
12
* Base strategy class from passport-strategy package
13
* All custom strategies must inherit from this class
14
*/
15
const Strategy = require("passport-strategy");
16
17
class CustomStrategy extends Strategy {
18
/**
19
* Create a new strategy instance
20
* @param {Object} [options] - Strategy configuration options
21
* @param {Function} verify - Verification callback function
22
*/
23
constructor(options, verify) {
24
super();
25
this.name = "strategy-name"; // Required: unique strategy name
26
// Additional initialization
27
}
28
29
/**
30
* Main authentication method - must be implemented
31
* @param {http.IncomingMessage} req - HTTP request object
32
* @param {Object} [options] - Authentication options
33
*/
34
authenticate(req, options) {
35
// Authentication logic implementation
36
}
37
}
38
```
39
40
### Strategy Interface
41
42
Strategies have access to several action methods for communicating authentication results:
43
44
```javascript { .api }
45
/**
46
* Signal successful authentication
47
* @param {Object} user - Authenticated user object
48
* @param {Object} [info] - Additional authentication info
49
*/
50
this.success(user, info);
51
52
/**
53
* Signal authentication failure
54
* @param {string} [challenge] - Authentication challenge message
55
* @param {number} [status=401] - HTTP status code
56
*/
57
this.fail(challenge, status);
58
59
/**
60
* Redirect to external authentication provider
61
* @param {string} url - Redirect URL
62
* @param {number} [status=302] - HTTP status code
63
*/
64
this.redirect(url, status);
65
66
/**
67
* Pass without making success/failure decision
68
* Used for session restoration and optional authentication
69
*/
70
this.pass();
71
72
/**
73
* Signal internal authentication error
74
* @param {Error} err - Error object
75
*/
76
this.error(err);
77
```
78
79
**Strategy Action Method Details:**
80
81
The strategy action methods are dynamically attached during authentication and handle complex logic including flash messages, redirects, session management, and error handling:
82
83
```javascript { .api }
84
/**
85
* Success method handles authentication success with extensive option processing
86
* - Processes successFlash, successMessage, and successRedirect options
87
* - Handles user property assignment via assignProperty option
88
* - Manages session login via req.logIn() when session enabled
89
* - Transforms auth info via passport.transformAuthInfo() when authInfo enabled
90
*/
91
strategy.success = function(user, info) {
92
// Internal implementation handles:
93
// - Flash message processing
94
// - Session messages
95
// - Property assignment
96
// - Session login
97
// - Auth info transformation
98
// - Redirect handling
99
};
100
101
/**
102
* Fail method handles authentication failure with challenge accumulation
103
* - Accumulates challenges from multiple strategies
104
* - Processes failureFlash and failureMessage options
105
* - Sets WWW-Authenticate header with challenges
106
* - Handles failureRedirect or failWithError options
107
*/
108
strategy.fail = function(challenge, status) {
109
// Internal implementation handles:
110
// - Challenge accumulation across strategies
111
// - Flash message processing
112
// - HTTP header setting
113
// - Error vs redirect handling
114
};
115
116
/**
117
* Redirect method handles external redirects (OAuth flows)
118
* - Sets appropriate HTTP status code (default 302)
119
* - Performs redirect to external authentication provider
120
*/
121
strategy.redirect = function(url, status) {
122
// Internal implementation performs HTTP redirect
123
};
124
125
/**
126
* Pass method allows strategy to defer decision
127
* - Used for optional authentication scenarios
128
* - Continues middleware chain without success/failure
129
* - Common in session strategies when no session data exists
130
*/
131
strategy.pass = function() {
132
// Internal implementation continues to next middleware
133
};
134
135
/**
136
* Error method handles internal authentication errors
137
* - Bypasses normal authentication flow
138
* - Passes error to Express error handling middleware
139
*/
140
strategy.error = function(err) {
141
// Internal implementation calls next(err)
142
};
143
```
144
145
### Custom Strategy Example
146
147
Here's a complete example of a custom API key authentication strategy:
148
149
```javascript { .api }
150
const Strategy = require("passport-strategy");
151
const util = require("util");
152
153
/**
154
* Custom API Key authentication strategy
155
* @param {Object} options - Strategy options
156
* @param {string} [options.apiKeyHeader='x-api-key'] - Header name for API key
157
* @param {Function} verify - Verification function (apiKey, done) => void
158
*/
159
function ApiKeyStrategy(options, verify) {
160
if (typeof options === "function") {
161
verify = options;
162
options = {};
163
}
164
if (!verify) {
165
throw new TypeError("ApiKeyStrategy requires a verify callback");
166
}
167
168
Strategy.call(this);
169
170
this.name = "apikey";
171
this._verify = verify;
172
this._apiKeyHeader = options.apiKeyHeader || "x-api-key";
173
}
174
175
// Inherit from Strategy
176
util.inherits(ApiKeyStrategy, Strategy);
177
178
/**
179
* Authenticate request using API key
180
* @param {http.IncomingMessage} req - HTTP request
181
* @param {Object} [options] - Authentication options
182
*/
183
ApiKeyStrategy.prototype.authenticate = function(req, options) {
184
const apiKey = req.headers[this._apiKeyHeader];
185
186
if (!apiKey) {
187
return this.fail("Missing API key", 401);
188
}
189
190
const self = this;
191
192
function verified(err, user, info) {
193
if (err) return self.error(err);
194
if (!user) return self.fail(info || "Invalid API key", 401);
195
return self.success(user, info);
196
}
197
198
try {
199
this._verify(apiKey, verified);
200
} catch (ex) {
201
return this.error(ex);
202
}
203
};
204
205
module.exports = ApiKeyStrategy;
206
```
207
208
**Usage of Custom Strategy:**
209
210
```javascript
211
const ApiKeyStrategy = require("./apikey-strategy");
212
213
// Register the custom strategy
214
passport.use(new ApiKeyStrategy(
215
{ apiKeyHeader: "authorization" },
216
async (apiKey, done) => {
217
try {
218
const user = await User.findOne({ apiKey: apiKey });
219
if (!user) {
220
return done(null, false, { message: "Invalid API key" });
221
}
222
return done(null, user);
223
} catch (err) {
224
return done(err);
225
}
226
}
227
));
228
229
// Use the strategy
230
app.get("/api/data",
231
passport.authenticate("apikey", { session: false }),
232
(req, res) => {
233
res.json({ data: "protected data", user: req.user.id });
234
}
235
);
236
```
237
238
### OAuth Strategy Pattern
239
240
OAuth strategies typically involve redirection to external providers:
241
242
```javascript { .api }
243
const Strategy = require("passport-strategy");
244
const OAuth2 = require("oauth").OAuth2;
245
246
/**
247
* Custom OAuth2 strategy example
248
* @param {Object} options - OAuth2 configuration
249
* @param {Function} verify - Verification callback
250
*/
251
function CustomOAuth2Strategy(options, verify) {
252
Strategy.call(this);
253
254
this.name = "custom-oauth2";
255
this._verify = verify;
256
this._oauth2 = new OAuth2(
257
options.clientID,
258
options.clientSecret,
259
options.authorizationURL,
260
options.tokenURL
261
);
262
this._callbackURL = options.callbackURL;
263
}
264
265
util.inherits(CustomOAuth2Strategy, Strategy);
266
267
CustomOAuth2Strategy.prototype.authenticate = function(req, options) {
268
if (req.query && req.query.code) {
269
// Handle callback from OAuth provider
270
this._oauth2.getOAuthAccessToken(
271
req.query.code,
272
{ grant_type: "authorization_code" },
273
(err, accessToken, refreshToken, results) => {
274
if (err) return this.error(err);
275
276
// Get user profile with access token
277
this._oauth2.get(
278
"https://api.provider.com/user",
279
accessToken,
280
(err, body) => {
281
if (err) return this.error(err);
282
283
const profile = JSON.parse(body);
284
this._verify(accessToken, refreshToken, profile, (err, user) => {
285
if (err) return this.error(err);
286
if (!user) return this.fail();
287
return this.success(user);
288
});
289
}
290
);
291
}
292
);
293
} else {
294
// Redirect to OAuth provider
295
const authURL = this._oauth2.getAuthorizeUrl({
296
response_type: "code",
297
client_id: this._oauth2._clientId,
298
redirect_uri: this._callbackURL,
299
scope: options.scope || "read"
300
});
301
this.redirect(authURL);
302
}
303
};
304
```
305
306
### Built-in Session Strategy
307
308
Understanding the built-in SessionStrategy implementation:
309
310
```javascript { .api }
311
/**
312
* Built-in session strategy for restoring authentication from session
313
* @param {Object|Function} [options] - Strategy options or deserializeUser function if only one parameter
314
* @param {string} [options.key='passport'] - Session key
315
* @param {Function} deserializeUser - User deserialization function
316
*/
317
class SessionStrategy extends Strategy {
318
constructor(options, deserializeUser);
319
constructor(deserializeUser);
320
name: "session";
321
322
/**
323
* Authenticate based on session data
324
* @param {http.IncomingMessage} req - HTTP request
325
* @param {Object} [options] - Authentication options
326
* @param {boolean} [options.pauseStream=false] - Pause request stream
327
*/
328
authenticate(req, options);
329
}
330
```
331
332
### Strategy Testing
333
334
Example of how to test custom strategies:
335
336
```javascript
337
const chai = require("chai");
338
const expect = chai.expect;
339
const ApiKeyStrategy = require("../lib/apikey-strategy");
340
341
describe("ApiKeyStrategy", () => {
342
let strategy;
343
344
beforeEach(() => {
345
strategy = new ApiKeyStrategy((apiKey, done) => {
346
if (apiKey === "valid-key") {
347
return done(null, { id: 1, name: "Test User" });
348
}
349
return done(null, false);
350
});
351
});
352
353
it("should authenticate with valid API key", (done) => {
354
const req = {
355
headers: { "x-api-key": "valid-key" }
356
};
357
358
strategy.success = (user) => {
359
expect(user).to.deep.equal({ id: 1, name: "Test User" });
360
done();
361
};
362
363
strategy.authenticate(req);
364
});
365
366
it("should fail with invalid API key", (done) => {
367
const req = {
368
headers: { "x-api-key": "invalid-key" }
369
};
370
371
strategy.fail = (challenge, status) => {
372
expect(challenge).to.equal("Invalid API key");
373
expect(status).to.equal(401);
374
done();
375
};
376
377
strategy.authenticate(req);
378
});
379
380
it("should fail with missing API key", (done) => {
381
const req = { headers: {} };
382
383
strategy.fail = (challenge, status) => {
384
expect(challenge).to.equal("Missing API key");
385
expect(status).to.equal(401);
386
done();
387
};
388
389
strategy.authenticate(req);
390
});
391
});
392
```
393
394
## Advanced Strategy Patterns
395
396
### Multi-step Authentication
397
398
Strategies can implement multi-step authentication flows:
399
400
```javascript
401
class TwoFactorStrategy extends Strategy {
402
constructor(options, verify) {
403
super();
404
this.name = "twofactor";
405
this._verify = verify;
406
}
407
408
authenticate(req, options) {
409
const { username, password, token } = req.body;
410
411
if (!username || !password) {
412
return this.fail("Missing credentials", 400);
413
}
414
415
// First step: verify username/password
416
this._verify(username, password, (err, user) => {
417
if (err) return this.error(err);
418
if (!user) return this.fail("Invalid credentials", 401);
419
420
if (!token) {
421
// Request second factor
422
return this.fail("Two-factor token required", 200);
423
}
424
425
// Second step: verify 2FA token
426
if (this.verifyToken(user, token)) {
427
return this.success(user);
428
} else {
429
return this.fail("Invalid token", 401);
430
}
431
});
432
}
433
434
verifyToken(user, token) {
435
// Implement 2FA token verification
436
return user.twoFactorSecret &&
437
require("speakeasy").totp.verify({
438
secret: user.twoFactorSecret,
439
encoding: "base32",
440
token: token
441
});
442
}
443
}
444
```
445
446
### Conditional Authentication
447
448
Strategies can implement conditional logic:
449
450
```javascript
451
class SmartStrategy extends Strategy {
452
constructor(options, verify) {
453
super();
454
this.name = "smart";
455
this._verify = verify;
456
}
457
458
authenticate(req, options) {
459
const ipAddress = req.ip;
460
const userAgent = req.headers["user-agent"];
461
462
// Check if this is a trusted environment
463
if (this.isTrustedEnvironment(ipAddress, userAgent)) {
464
// Skip authentication for trusted environments
465
return this.pass();
466
}
467
468
// Require authentication for untrusted environments
469
const token = this.extractToken(req);
470
if (!token) {
471
return this.fail("Authentication required", 401);
472
}
473
474
this._verify(token, (err, user) => {
475
if (err) return this.error(err);
476
if (!user) return this.fail("Invalid token", 401);
477
return this.success(user);
478
});
479
}
480
481
isTrustedEnvironment(ip, userAgent) {
482
// Implement trust logic
483
return ip.startsWith("192.168.") ||
484
userAgent.includes("TrustedApp");
485
}
486
487
extractToken(req) {
488
const authHeader = req.headers.authorization;
489
if (authHeader && authHeader.startsWith("Bearer ")) {
490
return authHeader.substring(7);
491
}
492
return null;
493
}
494
}
495
```