UNPKG

@trimble-oss/trimble-id

Version:

Trimble Identity SDK for JavaScript/ TypeScript

282 lines (256 loc) 13.6 kB
'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; }));