@trimble-oss/trimble-id
Version:
Trimble Identity SDK for JavaScript/ TypeScript
282 lines (256 loc) • 13.6 kB
JavaScript
'use strict';
// implements ITokenProvider
(function (root, factory) {
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) {
// AMD
define([
'./RefreshableTokenProvider',
'./HttpClient',
'./AnalyticsHttpClient',
'jsrsasign',
], (RefreshableTokenProvider, HttpClient, AnalyticsHttpClient, rs) => {
return factory({
RefreshableTokenProvider: RefreshableTokenProvider,
HttpClient: HttpClient,
AnalyticsHttpClient: AnalyticsHttpClient,
KJUR: rs.KJUR,
stob64: rs.stob64,
hextob64u: rs.hextob64u
});
});
} else if (typeof exports === 'object') {
// CommonJS
var rs = require('jsrsasign');
module.exports = factory({
RefreshableTokenProvider: require('./RefreshableTokenProvider'),
HttpClient: require('./HttpClient'),
AnalyticsHttpClient: require('./AnalyticsHttpClient'),
KJUR: rs.KJUR,
stob64: rs.stob64,
hextob64u: rs.hextob64u
});
} else {
// Browser globals (Note: root is window)
root.AuthorizationCodeGrantTokenProvider = factory(root);
}
}(this, function (imports) {
/**
* @implements {ITokenProvider}
* @description The Authorization Code grant type is intended to be used by user-facing web applications with a server-side component. When the user grants authorization, the Identity authorization endpoint provides the client with a short-lived authorization code through a browser redirect. The client subsequently exchanges the authorization_code for an access_token.
* The Authorization Code with Proof Key for Code Exchange (PKCE) flow is an extension of the Authorization Code grant flow. Along with the request, the client application sends code_challenge and code_challenge_method.
*/
class AuthorizationCodeGrantTokenProvider extends imports.RefreshableTokenProvider {
/**
* @description Public constructor for AuthorizationCodeGrantTokenProvider class
* @param {IEndpointProvider} endpointProvider An endpoint provider that provides the URL for the Trimble Identity authorization and token endpoints.
* It can be OpenIdEndpointProvider/FixedEndpointProvider
* @param {string} consumerKey The consumer key for the calling application
* @param {string} redirectUrl The URL to which Trimble Identity should redirect after successfully authenticating a user
*/
constructor(endpointProvider, consumerKey, redirectUrl) {
super(endpointProvider, consumerKey, null, null, null, null, null);
this._redirectUrl = redirectUrl
this._codeVerifier = null;
this._scopes = ['openid'];
this._state = null;
//Send analytics
this._analyticshttpclient = imports.AnalyticsHttpClient;
this._analyticshttpclient.sendInitEvent(this.constructor.name, consumerKey);
}
/**
* @description Fluent extension to add scopes
* @param {IEnumerable<string>} scopes The scopes to add to the token provider
*/
WithScopes(scopes) {
this._scopes = this._scopes.concat(scopes);
return this;
}
/**
* @description Fluent extension to add logout redirect URL
* @param {string} logoutRedirectUrl
*/
WithLogoutRedirect(logoutRedirectUrl) {
this._logoutRedirectUrl = logoutRedirectUrl;
return this;
}
/**
* @description Fluent extension to add identity provider
* @param {string} identityProvider
*/
WithIdentityProvider(identityProvider) {
this._identityProvider = identityProvider;
return this;
}
/**
* @description Get a redirect URL for Trimble Identity
* @param {string} state An optional state parameter that will be passed back to the caller via the redirect URL
* @returns {PromiseLike<string>} A promise that resolves to the redirect URL
* @exception Thrown when an authorization endpoint is not provided by the endpoint provider
*/
GetOAuthRedirect(state) {
//Send analytics
this._analyticshttpclient.sendMethodEvent(this.GetOAuthRedirect.name, this._consumerKey);
var self = this;
return new Promise((resolve, reject) => {
self._endpointProvider.RetrieveAuthorizationEndpoint()
.then((endpoint) => {
var url = endpoint +
'?response_type=code' +
'&scope=' + encodeURIComponent(self._scopes.join(' ')) +
'&client_id=' + encodeURIComponent(self._consumerKey) +
'&redirect_uri=' + encodeURIComponent(self._redirectUrl);
if (self._codeVerifier) {
url = url +
'&code_challenge_method=S256' +
'&code_challenge=' + encodeURIComponent(self._GenerateCodeChallenge(self._codeVerifier));
}
if (state) {
url = url +
'&state=' + encodeURIComponent(state);
}
if (self._identityProvider) {
url = url +
'&identity_provider=' + encodeURIComponent(self._identityProvider);
}
resolve(url);
})
.catch((err) => {
self._analyticshttpclient.sendExceptionEvent(self.GetOAuthRedirect.name, err, self._consumerKey);
reject('Unable to return OAuth redirect: ' + err);
});
});
}
/**
* @description Validate the query parameters passed back to the application by Trimble Identity
* @param {string} query The query string from the URL
* @returns {PromiseLike<boolean>} A promise that resolves to true if the query string is valid
* @exception Thrown when a token endpoint is not provided by the endpoint provider
* @exception Thrown when a call to the token endpoint fails
*/
ValidateQuery(query) {
//Send analytics
this._analyticshttpclient.sendMethodEvent(this.ValidateQuery.name, this._consumerKey);
/* istanbul ignore else */
if (query.startsWith('?'))
query = query.substr(1);
var values = {};
query.split('&').forEach((parameter) => {
var keyValue = parameter.split('=');
values[keyValue[0]] = decodeURIComponent(keyValue[1]);
});
this._state = values.state;
return this._validateCode(values.code);
}
/**
* @description Return a redirect URL to log out of all Trimble Identity applications
* @param {string} state An optional state parameter that will be passed back to the caller via the redirect URL
* @returns {PromiseLike<string>} A promise that resolves to the value of the redirect URL on completion
*/
GetOAuthLogoutRedirect(state = null) {
//Send analytics
this._analyticshttpclient.sendMethodEvent(this.GetOAuthLogoutRedirect.name, this._consumerKey);
var self = this;
return new Promise((resolve, reject) => {
self._endpointProvider.RetrieveEndSessionEndpoint()
.then((endpoint) => {
// Trimble Identity v3 handles logout differently to Trimble Identity v4
if (endpoint.endsWith("/commonauth")) // v3
resolve(endpoint +
"?commonAuthLogout=true" +
"&type=samlsso" +
"&relyingParty=" + encodeURIComponent(self._consumerKey) +
"&commonAuthCallerPath=" + encodeURIComponent(self._logoutRedirectUrl)
);
else // v4+
{
self.RetrieveIdToken()
.then((idToken) => {
var url = endpoint +
"?id_token_hint=" + encodeURIComponent(idToken) +
"&post_logout_redirect_uri=" + encodeURIComponent(self._logoutRedirectUrl);
if (state) {
url = url +
"&state=" + encodeURIComponent(state);
}
resolve(url);
})
.catch((error) => {
self._analyticshttpclient.sendExceptionEvent(self.GetOAuthLogoutRedirect.name, error, self._consumerKey);
reject(error)
});
}
})
.catch((error) => {
self._analyticshttpclient.sendExceptionEvent(self.GetOAuthLogoutRedirect.name, error, self._consumerKey);
reject(error)
});
});
}
/**
* @description Validate the code passed back to the application by Trimble Identity
* @param {string} code from the URL
* @returns {PromiseLike<boolean>} A promise that resolves to true if the code is valid
* @exception Thrown when a token endpoint is not provided by the endpoint provider
* @exception Thrown when a call to the token endpoint fails
*/
ValidateCode(code) {
//Send analytics
this._analyticshttpclient.sendMethodEvent(this.ValidateCode.name, this._consumerKey);
return this._validateCode(code);
}
_validateCode(code) {
var self = this;
return new Promise(function (resolve, reject) {
self._endpointProvider.RetrieveTokenEndpoint()
.then((endpoint) => {
var requestSettings = {
headers: {
"Content-Type": 'application/x-www-form-urlencoded',
Accept: 'application/json',
}
};
if (self._consumerSecret) {
var basicHeader = 'Basic ' + imports.stob64(self._consumerKey + ':' + self._consumerSecret);
requestSettings.headers.Authorization = basicHeader;
}
var content = 'grant_type=authorization_code' +
'&tenantDomain=trimble.com' +
'&client_id=' + encodeURIComponent(self._consumerKey) +
'&code=' + encodeURIComponent(code) +
'&redirect_uri=' + encodeURIComponent(self._redirectUrl);
var newCodeVerifier = null;
if (self._codeVerifier) {
content = content + '&code_verifier=' + encodeURIComponent(self._codeVerifier);
newCodeVerifier = imports.RefreshableTokenProvider.GenerateCodeVerifier();
content = content + '&code_challenge_method=S256';
content = content + '&code_challenge=' + encodeURIComponent(self._GenerateCodeChallenge(newCodeVerifier));
}
new imports.HttpClient().httpPost(endpoint, content, requestSettings)
.then((json) => {
var result = JSON.parse(json);
var now = new Date();
self
.WithAccessToken(result.access_token, new Date(now.getTime() + result.expires_in * 1000))
.WithIdToken(result.id_token)
.WithRefreshToken(result.refresh_token);
if (newCodeVerifier) {
self.WithProofKeyForCodeExchange(newCodeVerifier);
}
resolve(true);
})
.catch((err) => {
self._analyticshttpclient.sendExceptionEvent(self.ValidateCode.name, err, self._consumerKey);
reject('Unable to validate code: ' + err);
});
})
.catch((err) => {
self._analyticshttpclient.sendExceptionEvent(self.ValidateCode.name, err, self._consumerKey);
reject('Unable to validate code: ' + err);
});
});
}
}
// Exposed public methods
return AuthorizationCodeGrantTokenProvider;
}));