-
-
Notifications
You must be signed in to change notification settings - Fork 110
/
digest.js
298 lines (269 loc) · 9.21 KB
/
digest.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/**
* Module dependencies.
*/
var passport = require('passport-strategy')
, crypto = require('crypto')
, util = require('util');
/**
* `DigestStrategy` constructor.
*
* The HTTP Digest authentication strategy authenticates requests based on
* username and digest credentials contained in the `Authorization` header
* field.
*
* Applications must supply a `secret` callback, which is used to look up the
* user and corresponding password (aka shared secret) known to both the server
* and the client, supplying them to the `done` callback as `user` and
*`password`, respectively. The strategy will use the password to compute the
* response hash, failing authentication if it does not match that found in the
* request. If the username is not valid, `user` should be set to false. If an
* exception occured, `err` should be set.
*
* An optional `validate` callback can be supplied, which receives `params`
* containing nonces that the server may want to track and validate.
*
* Options:
* - `realm` authentication realm, defaults to "Users"
* - `domain` list of URIs that define the protection space
* - `algorithm` algorithm used to produce the digest (MD5 | MD5-sess)
* - `qop` list of quality of protection values support by the server (auth | auth-int) (recommended: auth)
*
* `validate` params:
* - `nonce` unique string value specified by the server
* - `cnonce` opaque string value provided by the client
* - `nc` count of the number of requests (including the current request) that the client has sent with the nonce value
* - `opaque` string of data, specified by the server, which should be returned by the client in subsequent requests
*
* Examples:
*
* passport.use(new DigestStrategy({ qop: 'auth' },
* function(username, done) {
* // secret callback
* User.findOne({ username: username }, function (err, user) {
* if (err) { return done(err); }
* return done(null, user, user.password);
* });
* },
* function(params, done) {
* // validate callback, check nonces in params...
* done(err, true);
* }
* ));
*
* For further details on HTTP Basic authentication, refer to [RFC 2617: HTTP Authentication: Basic and Digest Access Authentication](http://tools.ietf.org/html/rfc2617)
*
* @param {Object} options
* @param {Function} secret
* @param {Function} validate
* @api public
*/
function DigestStrategy(options, secret, validate) {
if (typeof options == 'function') {
validate = secret;
secret = options;
options = {};
}
if (!secret) throw new Error('HTTP Digest authentication strategy requires a secret function');
passport.Strategy.call(this);
this.name = 'digest';
this._secret = secret;
this._validate = validate;
this._realm = options.realm || 'Users';
if (options.domain) {
this._domain = (Array.isArray(options.domain)) ? options.domain : [ options.domain ];
}
this._opaque = options.opaque;
this._algorithm = options.algorithm;
if (options.qop) {
this._qop = (Array.isArray(options.qop)) ? options.qop : [ options.qop ];
}
}
/**
* Inherit from `passport.Strategy`.
*/
util.inherits(DigestStrategy, passport.Strategy);
/**
* Authenticate request based on the contents of a HTTP Digest authorization
* header.
*
* @param {Object} req
* @api protected
*/
DigestStrategy.prototype.authenticate = function(req) {
var authorization = req.headers['authorization'];
if (!authorization) { return this.fail(this._challenge()); }
var parts = authorization.split(' ')
if (parts.length < 2) { return this.fail(400); }
var scheme = parts[0]
, params = parts.slice(1).join(' ');
if (!/Digest/i.test(scheme)) { return this.fail(this._challenge()); }
var creds = parse(params);
if (Object.keys(creds).length === 0) { return this.fail(400); }
if (!creds.username) {
return this.fail(this._challenge());
}
if (req.url !== creds.uri) {
return this.fail(400);
}
var self = this;
// Use of digest authentication requires a password (aka shared secret) known
// to both the client and server, but not transported over the wire. This
// secret is needed in order to compute the hashes required to authenticate
// the request, and can be obtained by calling the secret() function the
// application provides to the strategy. Because username is the key for a
// database query, a `user` instance is also obtained from this callback.
// However, the user will only be successfully authenticated if the password
// is correct, as indicated by the challenge response matching the computed
// value.
this._secret(creds.username, function(err, user, password) {
if (err) { return self.error(err); }
if (!user) { return self.fail(self._challenge()); }
var ha1;
if (!creds.algorithm || creds.algorithm === 'MD5') {
if (typeof password === 'object' && password.ha1) {
ha1 = password.ha1;
} else {
ha1 = md5(creds.username + ":" + creds.realm + ":" + password);
}
} else if (creds.algorithm === 'MD5-sess') {
// TODO: The nonce and cnonce used here should be the initial nonce
// value generated by the server and the first nonce value used by
// the client. This creates a 'session key' for the authentication
// of subsequent requests. The storage of the nonce values and the
// resulting session key needs to be investigated.
//
// See RFC 2617 (Section 3.2.2.2) for further details.
ha1 = md5(md5(creds.username + ":" + creds.realm + ":" + password) + ":" + creds.nonce + ":" + creds.cnonce);
} else {
return self.fail(400);
}
var ha2;
if (!creds.qop || creds.qop === 'auth') {
ha2 = md5(req.method + ":" + creds.uri);
} else if (creds.qop === 'auth-int') {
// TODO: Implement support for auth-int. Note that the raw entity body
// will be needed, not the parsed req.body property set by Connect's
// bodyParser middleware.
//
// See RFC 2617 (Section 3.2.2.3 and Section 3.2.2.4) for further
// details.
return self.error(new Error('auth-int not implemented'));
} else {
return self.fail(400);
}
var digest;
if (!creds.qop) {
digest = md5(ha1 + ":" + creds.nonce + ":" + ha2);
} else if (creds.qop === 'auth' || creds.qop === 'auth-int') {
digest = md5(ha1 + ":" + creds.nonce + ":" + creds.nc + ":" + creds.cnonce + ":" + creds.qop + ":" + ha2);
} else {
return self.fail(400);
}
if (creds.response != digest) {
return self.fail(self._challenge());
} else {
if (self._validate) {
self._validate({
nonce: creds.nonce,
cnonce: creds.cnonce,
nc: creds.nc,
opaque: creds.opaque
},
function(err, valid) {
if (err) { return self.error(err); }
if (!valid) { return self.fail(self._challenge()); }
self.success(user);
});
} else {
self.success(user);
}
}
});
}
/**
* Authentication challenge.
*
* @api private
*/
DigestStrategy.prototype._challenge = function() {
// TODO: For maximum flexibility, a mechanism for delegating the generation
// of the nonce and opaque data to the application would be useful.
var challenge = 'Digest realm="' + this._realm + '"';
if (this._domain) {
challenge += ', domain="' + this._domain.join(' ') + '"';
}
challenge += ', nonce="' + nonce(32) + '"';
if (this._opaque) {
challenge += ', opaque="' + this._opaque + '"';
}
if (this._algorithm) {
challenge += ', algorithm=' + this._algorithm;
}
if (this._qop) {
challenge += ', qop="' + this._qop.join(',') + '"';
}
return challenge;
}
/**
* Parse authentication response.
*
* @api private
*/
function parse(params) {
var opts = {};
var tokens = params.split(/,(?=(?:[^"]|"[^"]*")*$)/);
for (var i = 0, len = tokens.length; i < len; i++) {
var param = /(\w+)=["]?([^"]+)["]?$/.exec(tokens[i])
if (param) {
opts[param[1]] = param[2];
}
}
return opts;
}
/**
* Return a unique nonce with the given `len`.
*
* utils.uid(10);
* // => "FDaS435D2z"
*
* CREDIT: Connect -- utils.uid
* https://github.com/senchalabs/connect/blob/1.7.1/lib/utils.js
*
* @param {Number} len
* @return {String}
* @api private
*/
function nonce(len) {
var buf = []
, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
, charlen = chars.length;
for (var i = 0; i < len; ++i) {
buf.push(chars[Math.random() * charlen | 0]);
}
return buf.join('');
};
/**
* Return md5 hash of the given string and optional encoding,
* defaulting to hex.
*
* utils.md5('wahoo');
* // => "e493298061761236c96b02ea6aa8a2ad"
*
* CREDIT: Connect -- utils.md5
* https://github.com/senchalabs/connect/blob/1.7.1/lib/utils.js
*
* @param {String} str
* @param {String} encoding
* @return {String}
* @api private
*/
function md5(str, encoding){
return crypto
.createHash('md5')
.update(str)
.digest(encoding || 'hex');
};
/**
* Expose `DigestStrategy`.
*/
module.exports = DigestStrategy;