UNPKG

@trimble-oss/trimble-id

Version:

Trimble Identity SDK for JavaScript/ TypeScript

333 lines (304 loc) 15 kB
'use strict'; (function (root, factory) { /* istanbul ignore next */ if (typeof define === 'function' && define.amd) { // AMD define(['./HttpClient', './AnalyticsHttpClient', 'btoa', 'jsrsasign'], (HttpClient, AnalyticsHttpClient, btoa, rs) => { return factory({ HttpClient: HttpClient, AnalyticsHttpClient: AnalyticsHttpClient, btoa: btoa, KJUR: rs.KJUR, stob64: rs.stob64, hextob64u: rs.hextob64u, b64utoutf8: rs.b64utoutf8 }); }); } else if (typeof exports === 'object') { // CommonJS var rs = require('jsrsasign'); module.exports = factory({ HttpClient: require('./HttpClient'), AnalyticsHttpClient: require('./AnalyticsHttpClient'), btoa: root.btoa || require('btoa'), KJUR: rs.KJUR, stob64: rs.stob64, hextob64u: rs.hextob64u, b64utoutf8: rs.b64utoutf8 }); } else { // Browser globals (Note: root is window) root.RefreshableTokenProvider = factory(root); } }(this, function (imports) { /** * @implements {ITokenProvider} * @description The Refresh Token grant type is used by clients to exchange a refresh token for an access token when the access token has expired. */ class RefreshableTokenProvider { /** * @description Static method to generate a code verifier * @returns {string} A code verifier string */ static GenerateCodeVerifier() { var bytes = imports.KJUR.crypto.Util.getRandomHexOfNbits(32 * 8); return imports.hextob64u(bytes); } /** * @description Public constructor for RefreshableTokenProvider class * @param {IEndpointProvider} endpointProvider An endpoint provider that provides the URL for the Trimble Identity token endpoint. * It can be be OpenIdEndpointProvider/FixedEndpointProvider * @param {string} consumerKey The consumer key for the calling application */ constructor(endpointProvider, consumerKey) { this._endpointProvider = endpointProvider; this._consumerKey = consumerKey; //Send analytics this._analyticshttpclient = imports.AnalyticsHttpClient; this._analyticshttpclient.sendInitEvent("RefreshableTokenProvider", consumerKey); } /** * @description Fluent extension for Authorization Code with PKCE * @param {string} codeVerifier The PKCE code verifier for the calling application */ WithProofKeyForCodeExchange(codeVerifier) { this._codeVerifier = codeVerifier; return this; } /** * @description Fluent extension for Authorization Code with client secret * @param {string} consumerSecret The consumer secret for the calling application */ WithConsumerSecret(consumerSecret) { this._consumerSecret = consumerSecret; return this; } /** * @description Fluent extension for Authorization Code with access token * @param {string} accessToken The access token for the calling application * @param {datetime} tokenExpiry The access token expiry for the calling application */ WithAccessToken(accessToken, tokenExpiry) { this._accessToken = accessToken; this._tokenExpiry = tokenExpiry; return this; } /** * @description Fluent extension for Authorization Code with id token * @param {string} idToken The ID token for the calling application */ WithIdToken(idToken) { this._idToken = idToken; return this; } /** * @description Fluent extension for Authorization Code with refresh token * @param {string} refreshToken The refresh token for the calling application */ WithRefreshToken(refreshToken) { this._refreshToken = refreshToken; return this; } /** * @description Fluent extension for Authorization Code with persistent storage * @param {IPersistentStorage} persistentStorage The persistent storage method for the calling application */ WithPersistentStorage(persistentStorage) { this._persistentStorage = persistentStorage; return this; } /** * @description Fluent extension to add logout redirect URL * @param {string} logoutRedirectUrl */ WithLogoutRedirect(logoutRedirectUrl) { this._logoutRedirectUrl = logoutRedirectUrl; return this; } /** * @description Retrieves an access token for the authenticated user * @returns {PromiseLike<string>} A Task that resolves to the value of the access token on completion * @exception Thrown when a token endpoint is not provided by the endpoint provider * @exception Thrown when a call to the token endpoint fails */ RetrieveToken() { //Send analytics this._analyticshttpclient.sendMethodEvent(this.RetrieveToken.name, this._consumerKey); var self = this; return new Promise(function (resolve, reject) { var timestampWithBuffer = new Date(new Date().getTime() + (5 * 60000)); if (self._accessToken == null || (self._isJwt(self._accessToken) && self._jwtExpiry(self._accessToken) < timestampWithBuffer) || (!self._isJwt(self._accessToken) && self._tokenExpiry < timestampWithBuffer)) self._refreshTokenInternal() .then(() => { resolve(self._accessToken); }) .catch((err) => { self._analyticshttpclient.sendExceptionEvent(self.RetrieveToken.name, err, self._consumerKey); reject('Unable to refresh token: ' + err); }); else resolve(self._accessToken); }); } /** * @description Retrieves an access token expiry for the authenticated user * @returns {PromiseLike<string>} A Task that resolves to the value of the access token expiry on completion */ RetrieveTokenExpiry() { //Send analytics this._analyticshttpclient.sendMethodEvent(this.RetrieveTokenExpiry.name, this._consumerKey); var self = this; return new Promise(function (resolve) { resolve(self._tokenExpiry); }); } /** * @description Retrieves an ID token for the authenticated user * @returns {PromiseLike<string>} A Task that resolves to the value of the ID token on completion * @exception Thrown when a token endpoint is not provided by the endpoint provider * @exception Thrown when a call to the token endpoint fails */ RetrieveIdToken() { //Send analytics this._analyticshttpclient.sendMethodEvent(this.RetrieveIdToken.name, this._consumerKey); var self = this; return new Promise(function (resolve, reject) { var timestampWithBuffer = new Date(new Date().getTime() + (5 * 60000)); if (self._idToken == null || self._jwtExpiry(self._idToken) < timestampWithBuffer) self._refreshTokenInternal() .then(() => { resolve(self._idToken); }) .catch((err) => { self._analyticshttpclient.sendExceptionEvent(self.RetrieveIdToken.name, err, self._consumerKey); reject('Unable to refresh token: ' + err); }); else resolve(self._idToken); }); } /** * @description Retrieves a refresh token for the authenticated user * @returns {PromiseLike<string>} A Task that resolves to the value of the refresh token on completion * @exception Thrown when a token endpoint is not provided by the endpoint provider * @exception Thrown when a call to the token endpoint fails */ RetrieveRefreshToken() { //Send analytics this._analyticshttpclient.sendMethodEvent(this.RetrieveRefreshToken.name, this._consumerKey); var self = this; return new Promise(function (resolve, reject) { resolve(self._refreshToken); }); } /** * @description Retrieves a code verifier for the authenticated user for PKCE grant * @returns {PromiseLike<string>} A Task that resolves to the value of the code verifier on completion * @exception Thrown when a token endpoint is not provided by the endpoint provider * @exception Thrown when a call to the token endpoint fails */ RetrieveCodeVerifier() { //Send analytics this._analyticshttpclient.sendMethodEvent(this.RetrieveCodeVerifier.name, this._consumerKey); var self = this; return new Promise(function (resolve, reject) { resolve(self._codeVerifier); }); } /** * @description Revokes a refresh token for the authenticated user * @returns {PromiseLike<void>} A Task that revokes refreshtoken * @exception Thrown when a token endpoint is not provided by the endpoint provider * @exception Thrown when a call to the token endpoint fails */ RevokeRefreshToken() { //Send analytics this._analyticshttpclient.sendMethodEvent(this.RevokeRefreshToken.name, this._consumerKey); var self = this; return new Promise((resolve, reject) => { self._endpointProvider.RetrieveTokenRevocationEndpoint() .then((endpoint) => { var basicHeader = 'Basic ' + imports.btoa(self._consumerKey + ':' + self._consumerSecret); var requestSettings = { headers: { Authorization: basicHeader, "Content-Type": 'application/x-www-form-urlencoded', Accept: 'application/json', } }; var content = 'token=' + encodeURIComponent(self._refreshToken) + '&token_type_hint=refresh_token' + '&client_id=' + encodeURIComponent(self._consumerKey); new imports.HttpClient().httpPost(endpoint, content, requestSettings) .then(function (json) { resolve(); }) .catch((err) => { self._analyticshttpclient.sendExceptionEvent(self.RevokeRefreshToken.name, err, self._consumerKey); reject('Token revocation failed: ' + err); }); }) .catch((err) => { self._analyticshttpclient.sendExceptionEvent(self.RevokeRefreshToken.name, err, self._consumerKey); reject('Unable to retrieve token revocation endpoint: ' + err); }); }); } _isJwt(token) { return token.includes('.'); } _jwtExpiry(token) { var a = token.split('.'); var payload = imports.KJUR.jws.JWS.readSafeJSONString(imports.b64utoutf8(a[1])); return new Date(payload.exp * 1000); } _refreshTokenInternal() { 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.btoa(self._consumerKey + ':' + self._consumerSecret); requestSettings.headers.Authorization = basicHeader; } var content = 'grant_type=refresh_token' + '&client_id=' + encodeURIComponent(self._consumerKey) + '&refresh_token=' + encodeURIComponent(self._refreshToken); var newCodeVerifier = null; if (self._codeVerifier) { content = content + '&code_verifier=' + encodeURIComponent(self._codeVerifier); newCodeVerifier = RefreshableTokenProvider.GenerateCodeVerifier(); content = content + '&code_challenge_method=S256'; content = content + '&code_challenge=' + encodeURIComponent(self._GenerateCodeChallenge(newCodeVerifier)); } new imports.HttpClient().httpPost(endpoint, content, requestSettings) .then(function (json) { var result = JSON.parse(json); self._accessToken = result.access_token; var now = new Date(); self._tokenExpiry = new Date(now.getTime() + result.expires_in * 1000); self._idToken = result.id_token; self._refreshToken = result.refresh_token; if (newCodeVerifier) { self._codeVerifier = newCodeVerifier; } resolve(); }) .catch((err) => { reject('Token refresh failed: ' + err); }); }) .catch((err) => { reject('Unable to retrieve token endpoint: ' + err); }); }); } _GenerateCodeChallenge(codeVerifier) { var hash = imports.KJUR.crypto.Util.hashString(codeVerifier, 'sha256'); return imports.hextob64u(hash); } } // Exposed public methods return RefreshableTokenProvider; }));