/*
|
* @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 constants = require('./constants');
|
var CacheDriver = require('./cache-driver');
|
var Logger = require('./log').Logger;
|
var Mex = require('./mex');
|
var OAuth2Client = require('./oauth2client');
|
var SelfSignedJwt = require('./self-signed-jwt');
|
var UserRealm = require('./user-realm');
|
var WSTrustRequest = require('./wstrust-request');
|
|
var OAuth2Parameters = constants.OAuth2.Parameters;
|
var TokenResponseFields = constants.TokenResponseFields;
|
var OAuth2GrantType = constants.OAuth2.GrantType;
|
var OAuth2Scope = constants.OAuth2.Scope;
|
var Saml = constants.Saml;
|
var AccountType = constants.UserRealm.AccountType;
|
var WSTrustVersion = constants.WSTrustVersion;
|
var DeviceCodeResponseParameters = constants.UserCodeResponseFields;
|
|
/**
|
* Constructs a new TokenRequest object.
|
* @constructor
|
* @private
|
* @param {object} callContext Contains any context information that applies to the request.
|
* @param {AuthenticationContext} authenticationContext
|
* @param {string} resource
|
* @param {string} clientId
|
* @param {string} redirectUri
|
*/
|
function TokenRequest(callContext, authenticationContext, clientId, resource, redirectUri) {
|
this._log = new Logger('TokenRequest', callContext._logContext);
|
this._callContext = callContext;
|
this._authenticationContext = authenticationContext;
|
this._resource = resource;
|
this._clientId = clientId;
|
this._redirectUri = redirectUri;
|
|
// This should be set at the beginning of getToken
|
// functions that have a userId.
|
this._userId = null;
|
|
this._userRealm = null;
|
this._pollingClient = {};
|
}
|
|
TokenRequest.prototype._createUserRealmRequest = function(username) {
|
return new UserRealm(this._callContext, username, this._authenticationContext.authority);
|
};
|
|
TokenRequest.prototype._createMex = function(mexEndpoint) {
|
return new Mex(this._callContext, mexEndpoint);
|
};
|
|
TokenRequest.prototype._createWSTrustRequest = function(wstrustEndpoint, appliesTo, wstrustEndpointVersion) {
|
return new WSTrustRequest(this._callContext, wstrustEndpoint, appliesTo, wstrustEndpointVersion);
|
};
|
|
TokenRequest.prototype._createOAuth2Client = function() {
|
return new OAuth2Client(this._callContext, this._authenticationContext._authority);
|
};
|
|
TokenRequest.prototype._createSelfSignedJwt = function() {
|
return new SelfSignedJwt(this._callContext, this._authenticationContext._authority, this._clientId);
|
};
|
|
TokenRequest.prototype._oauthGetToken = function(oauthParameters, callback) {
|
var client = this._createOAuth2Client();
|
client.getToken(oauthParameters, callback);
|
};
|
|
TokenRequest.prototype._oauthGetTokenByPolling = function(oauthParameters, refresh_interval, expires_in, callback){
|
var client = this._createOAuth2Client();
|
client.getTokenWithPolling(oauthParameters, refresh_interval, expires_in, callback);
|
this._pollingClient = client;
|
}
|
|
TokenRequest.prototype._createCacheDriver = function() {
|
return new CacheDriver(
|
this._callContext,
|
this._authenticationContext.authority,
|
this._resource,
|
this._clientId,
|
this._authenticationContext.cache,
|
this._getTokenWithTokenResponse.bind(this)
|
);
|
};
|
|
/**
|
* Used by the cache driver to refresh tokens.
|
* @param {TokenResponse} entry A token response to refresh.
|
* @param {string} resource The resource for which to get the token.
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype._getTokenWithTokenResponse = function(entry, resource, callback) {
|
this._log.verbose('Called to refresh a token from the cache.');
|
var refreshToken = entry[TokenResponseFields.REFRESH_TOKEN];
|
this._getTokenWithRefreshToken(refreshToken, resource, null, callback);
|
};
|
|
TokenRequest.prototype._createCacheQuery = function() {
|
var query = {
|
clientId : this._clientId
|
};
|
|
if (this._userId) {
|
query.userId = this._userId;
|
} else {
|
this._log.verbose('No userId passed for cache query.');
|
}
|
|
return query;
|
};
|
|
|
TokenRequest.prototype._getTokenWithCacheWrapper = function(callback, getTokenFunc) {
|
var self = this;
|
this._cacheDriver = this._createCacheDriver();
|
var cacheQuery = this._createCacheQuery();
|
this._cacheDriver.find(cacheQuery, function(err, token) {
|
if (err) {
|
self._log.warn('Attempt to look for token in cahce resulted in Error');
|
self._log.warn('Attempt to look for token in cache resulted in Error: ' + err.stack, true);
|
}
|
|
if (!token) {
|
self._log.verbose('No appropriate cached token found.');
|
getTokenFunc.call(self, function(err, tokenResponse) {
|
if (err) {
|
self._log.verbose('getTokenFunc returned with err');
|
callback(err, tokenResponse);
|
return;
|
}
|
|
self._log.verbose('Successfully retrieved token from authority');
|
self._cacheDriver.add(tokenResponse, function() {
|
callback(null, tokenResponse);
|
});
|
});
|
} else {
|
self._log.info('Returning cached token.');
|
callback(err, token);
|
}
|
});
|
};
|
|
/**
|
* Store token into cache.
|
* @param {object} tokenResponse Token response to be added into the cache.
|
*/
|
TokenRequest.prototype._addTokenIntoCache = function(tokenResponse, callback) {
|
this._cacheDriver = this._createCacheDriver();
|
this._log.verbose('Storing retrieved token into cache');
|
this._cacheDriver.add(tokenResponse, function(err) {
|
callback(err, tokenResponse);
|
});
|
};
|
|
/**
|
* Adds an OAuth parameter to the paramters object if the parameter is
|
* not null or undefined.
|
* @private
|
* @param {object} parameters OAuth parameters object.
|
* @param {string} key A member of the OAuth2Parameters constants.
|
* @param {object} value
|
*/
|
function _addParameterIfAvailable(parameters, key, value) {
|
if (value) {
|
parameters[key] = value;
|
}
|
}
|
|
/**
|
* Creates a set of basic, common, OAuthParameters based on values that the TokenRequest
|
* was created with.
|
* @private
|
* @param {string} grantType A member of the OAuth2GrantType constants.
|
* @return {object}
|
*/
|
TokenRequest.prototype._createOAuthParameters = function(grantType) {
|
var oauthParameters = {};
|
oauthParameters[OAuth2Parameters.GRANT_TYPE] = grantType;
|
|
if (OAuth2GrantType.AUTHORIZATION_CODE !== grantType &&
|
OAuth2GrantType.CLIENT_CREDENTIALS !== grantType &&
|
OAuth2GrantType.DEVICE_CODE != grantType) {
|
oauthParameters[OAuth2Parameters.SCOPE] = OAuth2Scope.OPENID;
|
}
|
|
_addParameterIfAvailable(oauthParameters, OAuth2Parameters.CLIENT_ID, this._clientId);
|
_addParameterIfAvailable(oauthParameters, OAuth2Parameters.RESOURCE, this._resource);
|
_addParameterIfAvailable(oauthParameters, OAuth2Parameters.REDIRECT_URI, this._redirectUri);
|
|
return oauthParameters;
|
};
|
|
/**
|
* Get's a token from AAD using a username and password
|
* @private
|
* @param {string} username
|
* @param {string} password
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype._getTokenUsernamePasswordManaged = function(username, password, callback) {
|
this._log.verbose('Acquiring token with username password for managed user');
|
|
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.PASSWORD);
|
|
oauthParameters[OAuth2Parameters.PASSWORD] = password;
|
oauthParameters[OAuth2Parameters.USERNAME] = username;
|
|
this._oauthGetToken(oauthParameters, callback);
|
};
|
|
/**
|
* Determines the OAuth SAML grant type to use based on the passed in TokenType
|
* that was returned from a RSTR.
|
* @param {string} wstrustResponse RSTR token type.
|
* @return {string} An OAuth grant type.
|
*/
|
TokenRequest.prototype._getSamlGrantType = function(wstrustResponse) {
|
var tokenType = wstrustResponse.tokenType;
|
switch (tokenType) {
|
case Saml.TokenTypeV1:
|
return OAuth2GrantType.SAML1;
|
case Saml.TokenTypeV2:
|
return OAuth2GrantType.SAML2;
|
default:
|
throw this._log.createError('RSTR returned unknown token type: ' + tokenType);
|
}
|
};
|
|
/**
|
* Performs an OAuth SAML Assertion grant type exchange. Uses a SAML token as the credential for getting
|
* an OAuth access token.
|
* @param {WSTrustResponse} wstrustResponse A response from a WSTrustRequest
|
* @param {AcquireTokenCallback} callback callback
|
*/
|
TokenRequest.prototype._performWSTrustAssertionOAuthExchange = function(wstrustResponse, callback) {
|
this._log.verbose('Performing OAuth assertion grant type exchange.');
|
|
var oauthParameters;
|
try {
|
var grantType = this._getSamlGrantType(wstrustResponse);
|
var assertion = new Buffer(wstrustResponse.token).toString('base64');
|
oauthParameters = this._createOAuthParameters(grantType);
|
oauthParameters[OAuth2Parameters.ASSERTION] = assertion;
|
} catch (err) {
|
callback(err);
|
return;
|
}
|
|
this._oauthGetToken(oauthParameters, callback);
|
};
|
|
/**
|
* Exchange a username and password for a SAML token from an ADFS instance via WSTrust.
|
* @param {string} wstrustEndpoint An url of an ADFS WSTrust endpoint.
|
* @param {string} wstrustEndpointVersion The version of the wstrust endpoint.
|
* @param {string} username username
|
* @param {string} password password
|
* @param {AcquireTokenCallback} callback callback
|
*/
|
TokenRequest.prototype._performWSTrustExchange = function(wstrustEndpoint, wstrustEndpointVersion, username, password, callback) {
|
var self = this;
|
var wstrust = this._createWSTrustRequest(wstrustEndpoint, 'urn:federation:MicrosoftOnline', wstrustEndpointVersion);
|
wstrust.acquireToken(username, password, function(rstErr, response) {
|
if (rstErr) {
|
callback(rstErr);
|
return;
|
}
|
|
if (!response.token) {
|
var rstrErr = self._log.createError('Unsucessful RSTR.\n\terror code: ' + response.errorCode + '\n\tfaultMessage: ' + response.faultMessage, true);
|
callback(rstrErr);
|
return;
|
}
|
|
callback(null, response);
|
});
|
};
|
|
/**
|
* Given a username and password this method invokes a WSTrust and OAuth exchange to get an access token.
|
* @param {string} wstrustEndpoint An url of an ADFS WSTrust endpoint.
|
* @param {string} username username
|
* @param {string} password password
|
* @param {AcquireTokenCallback} callback callback
|
*/
|
TokenRequest.prototype._performUsernamePasswordForAccessTokenExchange = function(wstrustEndpoint, wstrustEndpointVersion, username, password, callback) {
|
var self = this;
|
this._performWSTrustExchange(wstrustEndpoint, wstrustEndpointVersion, username, password, function(err, wstrustResponse) {
|
if (err) {
|
callback(err);
|
return;
|
}
|
|
self._performWSTrustAssertionOAuthExchange(wstrustResponse, callback);
|
});
|
};
|
|
/**
|
* Returns an Error object indicating that AAD did not return a WSTrust endpoint.
|
* @return {Error}
|
*/
|
TokenRequest.prototype._createADWSTrustEndpointError = function() {
|
return this._log.createError('AAD did not return a WSTrust endpoint. Unable to proceed.');
|
};
|
|
/**
|
* Gets an OAuth access token using a username and password via a federated ADFS instance.
|
* @param {string} username username
|
* @param {string} password password
|
* @param {AcquireTokenCallback} callback callback
|
*/
|
TokenRequest.prototype._getTokenUsernamePasswordFederated = function(username, password, callback) {
|
this._log.verbose('Acquiring token with username password for federated user');
|
|
var self = this;
|
if (!this._userRealm.federationMetadataUrl) {
|
this._log.warn('Unable to retrieve federationMetadataUrl from AAD. Attempting fallback to AAD supplied endpoint.');
|
|
if (!this._userRealm.federationActiveAuthUrl) {
|
callback(this._createADWSTrustEndpointError());
|
return;
|
}
|
|
var wstrustVersion = this._parseWStrustVersionFromFederationActiveAuthUrl(this._userRealm.federationActiveAuthUrl);
|
this._log.verbose('Wstrust endpoint version is: ' + wstrustVersion);
|
this._performUsernamePasswordForAccessTokenExchange(this._userRealm.federationActiveAuthUrl, wstrustVersion, username, password, callback);
|
return;
|
} else {
|
var mexEndpoint = this._userRealm.federationMetadataUrl;
|
this._log.verbose('Attempting mex');
|
this._log.verbose('Attempting mex at: ' + mexEndpoint, true);
|
var mex = this._createMex(mexEndpoint);
|
mex.discover(function(mexErr) {
|
var wstrustEndpoint;
|
wstrustVersion = WSTrustVersion.UNDEFINED;
|
if (mexErr) {
|
self._log.warn('MEX exchange failed. Attempting fallback to AAD supplied endpoint.');
|
|
wstrustEndpoint = self._userRealm.federationActiveAuthUrl;
|
wstrustVersion = self._parseWStrustVersionFromFederationActiveAuthUrl(self._userRealm.federationActiveAuthUrl);
|
if (!wstrustEndpoint) {
|
callback(self._createADWSTrustEndpointError());
|
return;
|
}
|
} else {
|
wstrustEndpoint = mex.usernamePasswordPolicy.url;
|
wstrustVersion = mex.usernamePasswordPolicy.version;
|
}
|
|
self._performUsernamePasswordForAccessTokenExchange(wstrustEndpoint, wstrustVersion, username, password, callback);
|
return;
|
});
|
}
|
};
|
|
/**
|
* Gets wstrust endpoint version from the federation active auth url.
|
* @private
|
* @param {string} federationActiveAuthUrl federationActiveAuthUrl
|
* @return {object} The wstrust endpoint version.
|
*/
|
TokenRequest.prototype._parseWStrustVersionFromFederationActiveAuthUrl = function(federationActiveAuthUrl) {
|
var wstrust2005Regex = /[/trust]?[2005][/usernamemixed]?/;
|
var wstrust13Regex = /[/trust]?[13][/usernamemixed]?/;
|
|
if (wstrust2005Regex.exec(federationActiveAuthUrl)) {
|
return WSTrustVersion.WSTRUST2005;
|
}
|
else if (wstrust13Regex.exec(federationActiveAuthUrl)) {
|
return WSTrustVersion.WSTRUST13;
|
}
|
|
return WSTrustVersion.UNDEFINED;
|
};
|
|
/**
|
* Decides whether the username represents a managed or a federated user and then
|
* obtains a token using the appropriate protocol flow.
|
* @private
|
* @param {string} username
|
* @param {string} password
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype.getTokenWithUsernamePassword = function(username, password, callback) {
|
this._log.info('Acquiring token with username password');
|
this._userId = username;
|
|
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
|
var self = this;
|
|
if(this._authenticationContext._authority._isAdfsAuthority) {
|
this._log.info('Skipping user realm discovery for ADFS authority');
|
|
self._getTokenUsernamePasswordManaged(username, password, getTokenCompleteCallback);
|
return;
|
}
|
|
this._userRealm = this._createUserRealmRequest(username);
|
this._userRealm.discover(function(err) {
|
if (err) {
|
getTokenCompleteCallback(err);
|
return;
|
}
|
|
switch(self._userRealm.accountType) {
|
case AccountType.Managed:
|
self._getTokenUsernamePasswordManaged(username, password, getTokenCompleteCallback);
|
return;
|
case AccountType.Federated:
|
self._getTokenUsernamePasswordFederated(username, password, getTokenCompleteCallback);
|
return;
|
default:
|
getTokenCompleteCallback(self._log.createError('Server returned an unknown AccountType: ' + self._userRealm.AccountType));
|
}
|
});
|
});
|
};
|
|
/**
|
* Obtains a token using client credentials
|
* @private
|
* @param {string} clientSecret
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype.getTokenWithClientCredentials = function(clientSecret, callback) {
|
this._log.info('Getting token with client credentials.');
|
|
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
|
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.CLIENT_CREDENTIALS);
|
|
oauthParameters[OAuth2Parameters.CLIENT_SECRET] = clientSecret;
|
|
this._oauthGetToken(oauthParameters, getTokenCompleteCallback);
|
});
|
};
|
|
/**
|
* Obtains a token using an authorization code.
|
* @private
|
* @param {string} authorizationCode
|
* @param {string} clientSecret
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype.getTokenWithAuthorizationCode = function(authorizationCode, clientSecret, callback) {
|
this._log.info('Getting token with auth code.');
|
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.AUTHORIZATION_CODE);
|
|
oauthParameters[OAuth2Parameters.CODE] = authorizationCode;
|
oauthParameters[OAuth2Parameters.CLIENT_SECRET] = clientSecret;
|
|
this._oauthGetToken(oauthParameters, callback);
|
};
|
|
/**
|
* Obtains a token using a refresh token.
|
* @param {string} refreshToken
|
* @param {string} resource
|
* @param {string} [clientSecret]
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype._getTokenWithRefreshToken = function(refreshToken, resource, clientSecret, callback) {
|
this._log.info('Getting a new token from a refresh token.');
|
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.REFRESH_TOKEN);
|
|
if (resource) {
|
oauthParameters[OAuth2Parameters.RESOURCE] = resource;
|
}
|
|
if (clientSecret) {
|
oauthParameters[OAuth2Parameters.CLIENT_SECRET] = clientSecret;
|
}
|
|
oauthParameters[OAuth2Parameters.REFRESH_TOKEN] = refreshToken;
|
|
this._oauthGetToken(oauthParameters, callback);
|
};
|
|
/**
|
* Obtains a token using a refresh token.
|
* @param {string} refreshToken
|
* @param {string} [clientSecret]
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype.getTokenWithRefreshToken = function(refreshToken, clientSecret, callback) {
|
this._getTokenWithRefreshToken(refreshToken, null, clientSecret, callback);
|
};
|
|
/**
|
* Obtains a token from the cache, refreshing it or using a MRRT if necessary.
|
* @param {string} [userId] The user associated with the cached token.
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype.getTokenFromCacheWithRefresh = function(userId, callback) {
|
var self = this;
|
this._log.info('Getting token from cache with refresh if necessary.');
|
|
this._userId = userId;
|
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
|
// If this method was called then no cached entry was found. Since
|
// this particular version of acquireToken can only retrieve tokens
|
// from the cache, return an error.
|
getTokenCompleteCallback(self._log.createError('Entry not found in cache.'));
|
});
|
};
|
|
/**
|
* Creates a self signed jwt.
|
* @param {string} authorityUrl
|
* @param {string} certificate A PEM encoded certificate private key.
|
* @param {string} thumbprint
|
* @return {string} A self signed JWT
|
*/
|
TokenRequest.prototype._createJwt = function(authorityUrl, certificate, thumbprint) {
|
var jwt;
|
var ssj = this._createSelfSignedJwt();
|
jwt = ssj.create(certificate, thumbprint);
|
if (!jwt) {
|
throw this._log.createError('Failed to create JWT');
|
}
|
|
return jwt;
|
};
|
|
/**
|
* Obtains a token via a certificate. The certificate is used to generate a self signed
|
* JWT token that is passed as a client_assertion.
|
* @param {string} certificate A PEM encoded certificate private key.
|
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
|
* @param {AcquireTokenCallback} callback
|
*/
|
TokenRequest.prototype.getTokenWithCertificate = function(certificate, thumbprint, callback) {
|
|
this._log.info('Getting a token via certificate.');
|
|
var authorityUrl = this._authenticationContext._authority;
|
|
var jwt;
|
try {
|
jwt = this._createJwt(authorityUrl, certificate, thumbprint);
|
} catch (err) {
|
callback(err);
|
return;
|
}
|
|
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.CLIENT_CREDENTIALS);
|
oauthParameters[OAuth2Parameters.CLIENT_ASSERTION_TYPE] = OAuth2GrantType.JWT_BEARER;
|
oauthParameters[OAuth2Parameters.CLIENT_ASSERTION] = jwt;
|
|
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
|
this._oauthGetToken(oauthParameters, getTokenCompleteCallback);
|
});
|
};
|
|
TokenRequest.prototype.getTokenWithDeviceCode = function(userCodeInfo, callback) {
|
this._log.info('Getting a token via device code');
|
var self = this;
|
|
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.DEVICE_CODE);
|
oauthParameters[OAuth2Parameters.CODE] = userCodeInfo[DeviceCodeResponseParameters.DEVICE_CODE];
|
|
var interval = userCodeInfo[DeviceCodeResponseParameters.INTERVAL];
|
var expires_in = userCodeInfo[DeviceCodeResponseParameters.EXPIRES_IN];
|
|
if (interval <= 0) {
|
callback(new Error('invalid refresh interval'));
|
}
|
|
this._oauthGetTokenByPolling(oauthParameters, interval, expires_in, function(err, tokenResponse) {
|
if (err) {
|
self._log.verbose('Token polling request returend with err.');
|
callback(err, tokenResponse);
|
}
|
else {
|
self._addTokenIntoCache(tokenResponse, callback);
|
}
|
});
|
};
|
|
TokenRequest.prototype.cancelTokenRequestWithDeviceCode = function() {
|
this._pollingClient.cancelPollingRequest();
|
};
|
|
module.exports = TokenRequest;
|