/* * @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;