/*
|
* @copyright
|
* Copyright © Microsoft Open Technologies, Inc.
|
*
|
* All Rights Reserved
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http: *www.apache.org/licenses/LICENSE-2.0
|
*
|
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
* OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
* ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
|
* PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
|
*
|
* See the Apache License, Version 2.0 for the specific language
|
* governing permissions and limitations under the License.
|
*/
|
|
'use strict';
|
|
var request = require('request');
|
|
var argument = require('./argument');
|
var log = require('./log');
|
var util = require('./util');
|
var HttpErrorCode = require('./constants').HttpError;
|
|
/*
|
* Constants
|
*/
|
var consts = {
|
AUTHORIZATION_URI : 'authorization_uri',
|
RESOURCE : 'resource',
|
WWW_AUTHENTICATE_HEADER : 'www-authenticate'
|
};
|
|
/**
|
* The AuthenticationParameters class holds the parameters that are parsed from an OAuth challenge
|
* in the www-authenticate header.
|
* @constructor
|
* @param {string} authorizationUri The URI of an authority that can issues tokens for the
|
* resource that issued the challenge.
|
* @param {string} resource The resource for a which a token should be requested from the authority.
|
*/
|
function AuthenticationParameters(authorizationUri, resource) {
|
this._authorizationUri = authorizationUri;
|
this._resource = resource;
|
}
|
|
/**
|
* The URI of an authority that can issues tokens for the resource that issued the challenge.
|
* @instance
|
* @memberOf AuthenticationParameters
|
* @type {string}
|
* @name authorizationUri
|
*/
|
Object.defineProperty(AuthenticationParameters.prototype, 'authorizationUri', {
|
get : function() {
|
return this._authorizationUri;
|
}
|
});
|
|
/**
|
* The resource for a which a token should be requested from the authority.
|
* This property may be undefined if the resource was not returned in the challenge.
|
* @instance
|
* @memberOf AuthenticationParameters
|
* @type {string}
|
* @name authorizationUri
|
*/
|
Object.defineProperty(AuthenticationParameters.prototype, 'resource', {
|
get : function() {
|
return this._resource;
|
}
|
});
|
|
var exports = {};
|
|
// The 401 challenge is a standard defined in RFC6750, which is based in part on RFC2617.
|
// The challenge has the following form.
|
// WWW-Authenticate : Bearer authorization_uri="https://login.windows.net/mytenant.com/oauth2/authorize",Resource_id="00000002-0000-0000-c000-000000000000"
|
|
// This regex is used to validate the structure of the challenge header.
|
// Match whole structure: ^\s*Bearer\s+([^,\s="]+?)="([^"]*?)"\s*(,\s*([^,\s="]+?)="([^"]*?)"\s*)*$
|
// ^ Start at the beginning of the string.
|
// \s*Bearer\s+ Match 'Bearer' surrounded by one or more amount of whitespace.
|
// ([^,\s="]+?) This cpatures the key which is composed of any characters except comma, whitespace or a quotes.
|
// = Match the = sign.
|
// "([^"]*?)" Captures the value can be any number of non quote characters. At this point only the first key value pair as been captured.
|
// \s* There can be any amount of white space after the first key value pair.
|
// ( Start a capture group to retrieve the rest of the key value pairs that are separated by commas.
|
// \s* There can be any amount of whitespace before the comma.
|
// , There must be a comma.
|
// \s* There can be any amount of whitespace after the comma.
|
// (([^,\s="]+?) This will capture the key that comes after the comma. It's made of a series of any character excpet comma, whitespace or quotes.
|
// = Match the equal sign between the key and value.
|
// " Match the opening quote of the value.
|
// ([^"]*?) This will capture the value which can be any number of non quote characters.
|
// " Match the values closing quote.
|
// \s* There can be any amount of whitespace before the next comma.
|
// )* Close the capture group for key value pairs. There can be any number of these.
|
// $ The rest of the string can be whitespace but nothing else up to the end of the string.
|
//
|
//
|
// In other some other languages the regex above would be all that was needed. However, in JavaScript the RegExp object does not
|
// return all of the captures in one go. So the regex above needs to be broken up so that captures can be retrieved
|
// iteratively.
|
//
|
|
function parseChallenge(challenge) {
|
// This regex checks the structure of the whole challenge header. The complete
|
// header needs to be checked for validity before we can be certain that
|
// we will succeed in pulling out the individual parts.
|
var bearerChallengeStructureValidation = /^\s*Bearer\s+([^,\s="]+?)="([^"]*?)"\s*(,\s*([^,\s="]+?)="([^"]*?)"\s*)*$/;
|
|
// This regex pulls out the key and value from the very first pair.
|
var firstKeyValuePairRegex = /^\s*Bearer\s+([^,\s="]+?)="([^"]*?)"\s*/;
|
|
// This regex is used to pull out all of the key value pairs after the first one.
|
// All of these begin with a comma.
|
var allOtherKeyValuePairRegex = /(?:,\s*([^,\s="]+?)="([^"]*?)"\s*)/g;
|
|
|
if (!bearerChallengeStructureValidation.test(challenge)) {
|
throw new Error('The challenge is not parseable as an RFC6750 OAuth2 challenge');
|
}
|
|
var challengeParameters = {};
|
for(var match = firstKeyValuePairRegex.exec(challenge);
|
match;
|
match = allOtherKeyValuePairRegex.exec(challenge)) {
|
|
challengeParameters[match[1]] = match[2];
|
}
|
|
return challengeParameters;
|
}
|
|
exports.AuthenticationParameters = AuthenticationParameters;
|
|
/**
|
* Creates an {@link AuthenticationParameters} object from the contents of a
|
* www-authenticate header received from a HTTP 401 response from a resource server.
|
* @param {string} challenge The content fo the www-authenticate header.
|
* @return {AuthenticationParameters} An AuthenticationParameters object containing the parsed values from the header.
|
*/
|
exports.createAuthenticationParametersFromHeader = function(challenge) {
|
argument.validateStringParameter(challenge, 'challenge');
|
|
var challengeParameters = parseChallenge(challenge);
|
|
var authorizationUri = challengeParameters[consts.AUTHORIZATION_URI];
|
if (!authorizationUri) {
|
throw new Error('Could not find \'authorization_uri\' in challenge header.');
|
}
|
|
var resource = challengeParameters[consts.RESOURCE];
|
|
return new AuthenticationParameters(authorizationUri, resource);
|
};
|
|
/**
|
* Create an {@link AuthenticationParameters} object from a node http.IncomingMessage
|
* object that was created as a result of a request to a resource server. This function
|
* expects the response to contain a HTTP 401 error code with a www-authenticate
|
* header.
|
* @param {http.IncomingMessage} response A response from a http request to a resource server.
|
* @return {AuthenticationParameters}
|
*/
|
exports.createAuthenticationParametersFromResponse = function(response) {
|
if (!response) {
|
throw new Error('Mising required parameter: response');
|
}
|
|
if (!response.statusCode) {
|
throw new Error('The response parameter does not have the expected HTTP statusCode field');
|
}
|
|
if (HttpErrorCode.UNAUTHORIZED !== response.statusCode) {
|
throw new Error('The response status code does not correspond to an OAuth challenge. ' +
|
'The statusCode is expected to be 401 but is: ' + response.statusCode);
|
}
|
|
if (!response.headers) {
|
throw new Error('There were no headers found in the response.');
|
}
|
|
var challenge = response.headers[consts.WWW_AUTHENTICATE_HEADER];
|
if (!challenge) {
|
throw new Error('The response does not contain a WWW-Authenticate header that can be used to determine the authority_uri and resource.');
|
}
|
|
return exports.createAuthenticationParametersFromHeader(challenge);
|
};
|
|
function validateUrlObject(url) {
|
if (!url || !url.href) {
|
throw new Error('Parameter is of wrong type: url');
|
}
|
}
|
|
/**
|
* This is the callback that is passed to all acquireToken variants below.
|
* @callback CreateAuthenticationParametersCallback
|
* @memberOf AuthenticationContext
|
* @param {Error} [error] If the request fails this parameter will contain an Error object.
|
* @param {AuthenticationParameters} [parameters] On a succesful request returns a {@link AuthenticationParameters}.
|
*/
|
|
/**
|
* Creates an {@link AuthenticationParameters} object by sending a get request
|
* to the url passed to this function, and parsing the resulting http 401
|
* response.
|
* @param {string|url} url The url of a resource server.
|
* @param {AuthenticationParameters} callback Called on error or request completion.
|
* @param {string} [correlationId] An optional correlationId to pass along with the request and to include in any logs.
|
*/
|
exports.createAuthenticationParametersFromUrl = function(url, callback, correlationId) {
|
argument.validateCallbackType(callback);
|
try {
|
if (!url) {
|
callback(new Error('Missing required parameter: url'));
|
return;
|
}
|
var challengeUrl;
|
if ('string' === typeof(url)) {
|
challengeUrl = url;
|
} else {
|
validateUrlObject(url);
|
challengeUrl = url.href;
|
}
|
|
var logContext = log.createLogContext(correlationId);
|
var logger = new log.Logger('AuthenticationParameters', logContext);
|
|
logger.verbose('Attempting to retrieve authentication parameters');
|
logger.verbose('Attempting to retrieve authentication parameters from: ' + challengeUrl, true);
|
var options = util.createRequestOptions( { _callContext : { _logContext: logContext } } );
|
request.get(challengeUrl, options, function(err, response) {
|
if (err) {
|
logger.error('Authentication parameters http get failed.', err, true);
|
callback(err);
|
return;
|
}
|
var parameters;
|
try {
|
parameters = exports.createAuthenticationParametersFromResponse(response);
|
} catch(creationErr) {
|
logger.error('Unable to parse response in to authentication paramaters.', creationErr, true);
|
callback(creationErr);
|
return;
|
}
|
callback(null, parameters);
|
});
|
} catch(err) {
|
callback(err);
|
return;
|
}
|
};
|
|
module.exports = exports;
|