1 // Copyright 2012 Joyent, Inc. All rights reserved.
3 var assert = require('assert-plus');
4 var util = require('util');
5 var utils = require('./utils');
11 var HASH_ALGOS = utils.HASH_ALGOS;
12 var PK_ALGOS = utils.PK_ALGOS;
13 var HttpSignatureError = utils.HttpSignatureError;
14 var InvalidAlgorithmError = utils.InvalidAlgorithmError;
15 var validateAlgorithm = utils.validateAlgorithm;
30 ///--- Specific Errors
33 function ExpiredRequestError(message) {
34 HttpSignatureError.call(this, message, ExpiredRequestError);
36 util.inherits(ExpiredRequestError, HttpSignatureError);
39 function InvalidHeaderError(message) {
40 HttpSignatureError.call(this, message, InvalidHeaderError);
42 util.inherits(InvalidHeaderError, HttpSignatureError);
45 function InvalidParamsError(message) {
46 HttpSignatureError.call(this, message, InvalidParamsError);
48 util.inherits(InvalidParamsError, HttpSignatureError);
51 function MissingHeaderError(message) {
52 HttpSignatureError.call(this, message, MissingHeaderError);
54 util.inherits(MissingHeaderError, HttpSignatureError);
56 function StrictParsingError(message) {
57 HttpSignatureError.call(this, message, StrictParsingError);
59 util.inherits(StrictParsingError, HttpSignatureError);
66 * Parses the 'Authorization' header out of an http.ServerRequest object.
68 * Note that this API will fully validate the Authorization header, and throw
69 * on any error. It will not however check the signature, or the keyId format
70 * as those are specific to your environment. You can use the options object
71 * to pass in extra constraints.
73 * As a response object you can expect this:
76 * "scheme": "Signature",
79 * "algorithm": "rsa-sha256",
84 * "signature": "base64"
86 * "signingString": "ready to be passed to crypto.verify()"
89 * @param {Object} request an http.ServerRequest.
90 * @param {Object} options an optional options object with:
91 * - clockSkew: allowed clock skew in seconds (default 300).
92 * - headers: required header names (def: date or x-date)
93 * - algorithms: algorithms to support (default: all).
94 * - strict: should enforce latest spec parsing
96 * @return {Object} parsed out object (see above).
97 * @throws {TypeError} on invalid input.
98 * @throws {InvalidHeaderError} on an invalid Authorization header error.
99 * @throws {InvalidParamsError} if the params in the scheme are invalid.
100 * @throws {MissingHeaderError} if the params indicate a header not present,
101 * either in the request headers from the params,
102 * or not in the params from a required header
104 * @throws {StrictParsingError} if old attributes are used in strict parsing
106 * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew.
108 parseRequest: function parseRequest(request, options) {
109 assert.object(request, 'request');
110 assert.object(request.headers, 'request.headers');
111 if (options === undefined) {
114 if (options.headers === undefined) {
115 options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
117 assert.object(options, 'options');
118 assert.arrayOfString(options.headers, 'options.headers');
119 assert.optionalNumber(options.clockSkew, 'options.clockSkew');
121 if (!request.headers.authorization)
122 throw new MissingHeaderError('no authorization header present in ' +
125 options.clockSkew = options.clockSkew || 300;
129 var state = State.New;
130 var substate = ParamsState.Name;
140 return this.params.algorithm.toUpperCase();
144 return this.params.keyId;
148 var authz = request.headers.authorization;
149 for (i = 0; i < authz.length; i++) {
150 var c = authz.charAt(i);
152 switch (Number(state)) {
155 if (c !== ' ') parsed.scheme += c;
156 else state = State.Params;
160 switch (Number(substate)) {
162 case ParamsState.Name:
163 var code = c.charCodeAt(0);
164 // restricted name of A-Z / a-z
165 if ((code >= 0x41 && code <= 0x5a) || // A-Z
166 (code >= 0x61 && code <= 0x7a)) { // a-z
168 } else if (c === '=') {
169 if (tmpName.length === 0)
170 throw new InvalidHeaderError('bad param format');
171 substate = ParamsState.Quote;
173 throw new InvalidHeaderError('bad param format');
177 case ParamsState.Quote:
180 substate = ParamsState.Value;
182 throw new InvalidHeaderError('bad param format');
186 case ParamsState.Value:
188 parsed.params[tmpName] = tmpValue;
189 substate = ParamsState.Comma;
195 case ParamsState.Comma:
198 substate = ParamsState.Name;
200 throw new InvalidHeaderError('bad param format');
205 throw new Error('Invalid substate');
210 throw new Error('Invalid substate');
215 if (!parsed.params.headers || parsed.params.headers === '') {
216 if (request.headers['x-date']) {
217 parsed.params.headers = ['x-date'];
219 parsed.params.headers = ['date'];
222 parsed.params.headers = parsed.params.headers.split(' ');
225 // Minimally validate the parsed object
226 if (!parsed.scheme || parsed.scheme !== 'Signature')
227 throw new InvalidHeaderError('scheme was not "Signature"');
229 if (!parsed.params.keyId)
230 throw new InvalidHeaderError('keyId was not specified');
232 if (!parsed.params.algorithm)
233 throw new InvalidHeaderError('algorithm was not specified');
235 if (!parsed.params.signature)
236 throw new InvalidHeaderError('signature was not specified');
238 // Check the algorithm against the official list
239 parsed.params.algorithm = parsed.params.algorithm.toLowerCase();
241 validateAlgorithm(parsed.params.algorithm);
243 if (e instanceof InvalidAlgorithmError)
244 throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' +
250 // Build the signingString
251 for (i = 0; i < parsed.params.headers.length; i++) {
252 var h = parsed.params.headers[i].toLowerCase();
253 parsed.params.headers[i] = h;
255 if (h === 'request-line') {
256 if (!options.strict) {
258 * We allow headers from the older spec drafts if strict parsing isn't
259 * specified in options.
261 parsed.signingString +=
262 request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
264 /* Strict parsing doesn't allow older draft headers. */
265 throw (new StrictParsingError('request-line is not a valid header ' +
266 'with strict parsing enabled.'));
268 } else if (h === '(request-target)') {
269 parsed.signingString +=
270 '(request-target): ' + request.method.toLowerCase() + ' ' +
273 var value = request.headers[h];
274 if (value === undefined)
275 throw new MissingHeaderError(h + ' was not in the request');
276 parsed.signingString += h + ': ' + value;
279 if ((i + 1) < parsed.params.headers.length)
280 parsed.signingString += '\n';
283 // Check against the constraints
285 if (request.headers.date || request.headers['x-date']) {
286 if (request.headers['x-date']) {
287 date = new Date(request.headers['x-date']);
289 date = new Date(request.headers.date);
291 var now = new Date();
292 var skew = Math.abs(now.getTime() - date.getTime());
294 if (skew > options.clockSkew * 1000) {
295 throw new ExpiredRequestError('clock skew of ' +
297 's was greater than ' +
298 options.clockSkew + 's');
302 options.headers.forEach(function (hdr) {
303 // Remember that we already checked any headers in the params
304 // were in the request, so if this passes we're good.
305 if (parsed.params.headers.indexOf(hdr) < 0)
306 throw new MissingHeaderError(hdr + ' was not a signed header');
309 if (options.algorithms) {
310 if (options.algorithms.indexOf(parsed.params.algorithm) === -1)
311 throw new InvalidParamsError(parsed.params.algorithm +
312 ' is not a supported algorithm');