@trimble-oss/trimble-id
Version:
Trimble Identity SDK for JavaScript/ TypeScript
333 lines (304 loc) • 15 kB
JavaScript
'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;
}));