UNPKG

@it-enterprise/jwtauthentication

Version:

JSON Web Token-Based authentication library

1,194 lines (1,117 loc) 41.9 kB
import { jwtDecode } from 'jwt-decode' import axios from 'axios' import { UserManager } from 'oidc-client-ts' // eslint-disable-next-line const MD5 = function(d){var r = M(V(Y(X(d),8*d.length)));return r.toLowerCase()};function M(d){for(var _,m="0123456789ABCDEF",f="",r=0;r<d.length;r++)_=d.charCodeAt(r),f+=m.charAt(_>>>4&15)+m.charAt(15&_);return f}function X(d){for(var _=Array(d.length>>2),m=0;m<_.length;m++)_[m]=0;for(m=0;m<8*d.length;m+=8)_[m>>5]|=(255&d.charCodeAt(m/8))<<m%32;return _}function V(d){for(var _="",m=0;m<32*d.length;m+=8)_+=String.fromCharCode(d[m>>5]>>>m%32&255);return _}function Y(d,_){d[_>>5]|=128<<_%32,d[14+(_+64>>>9<<4)]=_;for(var m=1732584193,f=-271733879,r=-1732584194,i=271733878,n=0;n<d.length;n+=16){var h=m,t=f,g=r,e=i;f=md5_ii(f=md5_ii(f=md5_ii(f=md5_ii(f=md5_hh(f=md5_hh(f=md5_hh(f=md5_hh(f=md5_gg(f=md5_gg(f=md5_gg(f=md5_gg(f=md5_ff(f=md5_ff(f=md5_ff(f=md5_ff(f,r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+0],7,-680876936),f,r,d[n+1],12,-389564586),m,f,d[n+2],17,606105819),i,m,d[n+3],22,-1044525330),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+4],7,-176418897),f,r,d[n+5],12,1200080426),m,f,d[n+6],17,-1473231341),i,m,d[n+7],22,-45705983),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+8],7,1770035416),f,r,d[n+9],12,-1958414417),m,f,d[n+10],17,-42063),i,m,d[n+11],22,-1990404162),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+12],7,1804603682),f,r,d[n+13],12,-40341101),m,f,d[n+14],17,-1502002290),i,m,d[n+15],22,1236535329),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+1],5,-165796510),f,r,d[n+6],9,-1069501632),m,f,d[n+11],14,643717713),i,m,d[n+0],20,-373897302),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+5],5,-701558691),f,r,d[n+10],9,38016083),m,f,d[n+15],14,-660478335),i,m,d[n+4],20,-405537848),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+9],5,568446438),f,r,d[n+14],9,-1019803690),m,f,d[n+3],14,-187363961),i,m,d[n+8],20,1163531501),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+13],5,-1444681467),f,r,d[n+2],9,-51403784),m,f,d[n+7],14,1735328473),i,m,d[n+12],20,-1926607734),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+5],4,-378558),f,r,d[n+8],11,-2022574463),m,f,d[n+11],16,1839030562),i,m,d[n+14],23,-35309556),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+1],4,-1530992060),f,r,d[n+4],11,1272893353),m,f,d[n+7],16,-155497632),i,m,d[n+10],23,-1094730640),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+13],4,681279174),f,r,d[n+0],11,-358537222),m,f,d[n+3],16,-722521979),i,m,d[n+6],23,76029189),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+9],4,-640364487),f,r,d[n+12],11,-421815835),m,f,d[n+15],16,530742520),i,m,d[n+2],23,-995338651),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+0],6,-198630844),f,r,d[n+7],10,1126891415),m,f,d[n+14],15,-1416354905),i,m,d[n+5],21,-57434055),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+12],6,1700485571),f,r,d[n+3],10,-1894986606),m,f,d[n+10],15,-1051523),i,m,d[n+1],21,-2054922799),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+8],6,1873313359),f,r,d[n+15],10,-30611744),m,f,d[n+6],15,-1560198380),i,m,d[n+13],21,1309151649),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+4],6,-145523070),f,r,d[n+11],10,-1120210379),m,f,d[n+2],15,718787259),i,m,d[n+9],21,-343485551),m=safe_add(m,h),f=safe_add(f,t),r=safe_add(r,g),i=safe_add(i,e)}return Array(m,f,r,i)}function md5_cmn(d,_,m,f,r,i){return safe_add(bit_rol(safe_add(safe_add(_,d),safe_add(f,i)),r),m)}function md5_ff(d,_,m,f,r,i,n){return md5_cmn(_&m|~_&f,d,_,r,i,n)}function md5_gg(d,_,m,f,r,i,n){return md5_cmn(_&f|m&~f,d,_,r,i,n)}function md5_hh(d,_,m,f,r,i,n){return md5_cmn(_^m^f,d,_,r,i,n)}function md5_ii(d,_,m,f,r,i,n){return md5_cmn(m^(_|~f),d,_,r,i,n)}function safe_add(d,_){var m=(65535&d)+(65535&_);return(d>>16)+(_>>16)+(m>>16)<<16|65535&m}function bit_rol(d,_){return d<<_|d>>>32-_} export default class Authentication { // graphql server url static _baseUrl // web server url static _webUrl // oidc manager static _oidc static _oidcTokenKey // tokens store keys static _refreshTokenKey static _accessTokenKey static _tempAccessToken static _needConfirmTokenKey // hooks handlers static _onError static _onBeforeLogin static _onBeforeRefresh static _onAfterRefresh static _getLang static _axiosInstance static _skipCheckLicense static _refreshUrl static _authTypesUrl static _loginUrl static _logoutUrl static _getAuthGuidUrl static _alternativeLoginUrl static _checkTempPassAltUrl static _closeSessionUrl static _delegatedUsersUrl static _delegatedRightsUrl static _applyDelegationUrl static _addDelegationUrl static _editDelegationUrl static _confirmpasswordUrl static _loginByPINCodeUrl static _loginByBarCodeUrl static _returnOidcToken static _logger = console static _needLockRefreshToken static _refreshTokenLocked static _validateTokenBasedOnClientTime static _closeSessionAutomatically static _closeSessionAutomaticallyTimer static _useCookieForRefreshToken static _useServerRememberMe static _fingerprint static config({ baseUrl, onError, onBeforeRefresh, onAfterRefresh, onBeforeLogin, webUrl, oidc, getLang, refreshUrl, authTypesUrl, loginUrl, getAuthGuidUrl, alternativeLoginUrl, checkTempPassAltUrl, closeSessionUrl, delegatedUsersUrl, delegatedRightsUrl, applyDelegationUrl, addDelegationUrl, editDelegationUrl, confirmpasswordUrl, skipCheckLicense, loginByPINCodeUrl, loginByBarCodeUrl, returnOidcToken, needLockRefreshToken, validateTokenBasedOnClientTime, logger, closeSessionAutomatically, afterSignOutAction, fingerprint, silentRequestTimeoutInSeconds, useCookieForRefreshToken, logoutUrl, useServerRememberMe }) { this._baseUrl = baseUrl this._webUrl = webUrl this._refreshTokenKey = 'refreshToken' + MD5(baseUrl) this._accessTokenKey = 'accessToken' + MD5(baseUrl) this._oidcTokenKey = 'oidcToken' + MD5(baseUrl) this._needConfirmTokenKey = 'needConfirmToken' + MD5(baseUrl) this._tokenClientTimeKey = 'tokenClientTime' + MD5(baseUrl) this._needToLogInKey = 'needToLogIn' this._onError = onError this._onBeforeLogin = onBeforeLogin this._onBeforeRefresh = onBeforeRefresh this._onAfterRefresh = onAfterRefresh this._getLang = getLang this._axiosInstance = axios this._skipCheckLicense = skipCheckLicense this._useCookieForRefreshToken = useCookieForRefreshToken this._useServerRememberMe = useServerRememberMe if (oidc) { this._oidc = new UserManager({ authority: oidc.authority, client_id: oidc.client, response_type: 'code', scope: `openid profile email phone ${oidc.addScope ? oidc.addScope : ''}`, redirect_uri: `${this._webUrl}callback.html`, post_logout_redirect_uri: `${this._webUrl}callbacksignout.html`, silent_redirect_uri: `${this._webUrl}callbacksilent.html`, automaticSilentRenew: true, filterProtocolClaims: true, loadUserInfo: true, clockSkew: 28800, //(4 часа) silentRequestTimeoutInSeconds: silentRequestTimeoutInSeconds || 10 }) } this._refreshUrl = refreshUrl || 'api/authentication/refresh' this._authTypesUrl = authTypesUrl || 'api/authentication/authTypes' this._loginUrl = loginUrl || 'api/authentication/login' this._logoutUrl = logoutUrl || 'api/authentication/logout' this._getAuthGuidUrl = getAuthGuidUrl || 'api/authentication/getAuthGuid' this._alternativeLoginUrl = alternativeLoginUrl || 'api/authentication/alternativelogin' this._checkTempPassAltUrl = checkTempPassAltUrl || 'api/authentication/checkTempPassAlt' this._closeSessionUrl = closeSessionUrl || 'api/authentication/closeSession' this._delegatedUsersUrl = delegatedUsersUrl || 'api/authentication/delegatedUsers' this._delegatedRightsUrl = delegatedRightsUrl || 'api/authentication/delegatedRights' this._applyDelegationUrl = applyDelegationUrl || 'api/authentication/applyDelegation' this._addDelegationUrl = addDelegationUrl || 'api/authentication/addDelegation' this._editDelegationUrl = editDelegationUrl || 'api/authentication/editDelegation' this._confirmpasswordUrl = confirmpasswordUrl || 'api/authentication/confirmpassword' this._loginByPINCodeUrl = loginByPINCodeUrl || 'api/authentication/loginbypincode' this._loginByBarCodeUrl = loginByBarCodeUrl || 'api/authentication/loginbybarcode' this._returnOidcToken = returnOidcToken this._needLockRefreshToken = needLockRefreshToken this._refreshTokenLocked = false this._validateTokenBasedOnClientTime = validateTokenBasedOnClientTime this._logger = logger || console this._closeSessionAutomatically = closeSessionAutomatically this._afterSignOutAction = afterSignOutAction this._fingerprint = fingerprint const that = this this._axiosInstance.interceptors.request.use(function (config) { if (that._getLang) { config.headers['Accept-Language'] = that._getLang() } if (that._skipCheckLicense) { config.headers['skipCheckLicense'] = '+' } return config }) return this } //////////////////////////////// /// AUTHENTICATION FUNCTIONS /// //////////////////////////////// // GET ALLOWED AUTH TYPES static async getAllowedAuthTypes() { const url = this._createUrl(this._authTypesUrl) try { const response = await this._post(url) return JSON.parse(response.data) } catch (e) { if (this._onError) { this._onError(e) } this._logger.error(e.message) return null } } // LOGIN METHOD static async login(login, password, rememberMe, needCheckUserGroup = false) { let result = { success: false, errorMessage: '' } const fingerprint = await this._getFingerprint() if (this._onBeforeLogin) { this._onBeforeLogin(login, password, rememberMe, fingerprint) } const url = this._createUrl(this._loginUrl) try { const response = await this._post(url, { login, password, rememberMe, fingerprint, needCheckUserGroup }) if (response.data.Success) { this._setTokens(response, rememberMe) result.success = true this._tempAccessToken = response.data.AccessToken result.TempPasswordMessage = response.data.TempPasswordMessage result.TempPasswordRequired = response.data.TempPasswordRequired result.NeedChangePassword = response.data.NeedChangePassword if (response.data.TempPasswordRequired) { sessionStorage.setItem(this._needConfirmTokenKey, true) } else { sessionStorage.removeItem(this._needConfirmTokenKey) } } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } return result } static async confirmCode(confirmCode) { let result = { success: false, errorMessage: '' } if (confirmCode) { const url = this._createUrl(this._confirmpasswordUrl) const options = { headers: { 'Authorization': `Bearer ${this._tempAccessToken}` } } try { const response = await this._post(url, { confirmCode: confirmCode }, options) if (response.data.Success) { this._setTokens(response) result.success = true sessionStorage.removeItem(this._needConfirmTokenKey) } else { sessionStorage.setItem(this._needConfirmTokenKey, true) const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { this._logger.warn('Invalid confirmation code') result.errorMessage = 'Invalid confirmation code' } return result } // GET GUID FOR DIGITAL SIGNATURE AUTHENTICATION static async getAuthGuid() { const url = this._createUrl(this._getAuthGuidUrl) try { const response = await this._post(url) let data = JSON.parse(response.data) return data.GUID } catch (e) { this._errorHandler(e, '') } } // ALTERNATIVE LOGIN METHOD (QR-CODE) static async loginByDigitalSignature(sign) { let result = { success: false, errorMessage: '' } if (sign) { const url = this._createUrl(this._alternativeLoginUrl) try { const response = await this._post(url, { code: sign, type: 5}) if (response.data.Success) { this._setTokens(response) result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { this._logger.warn('Digital signature not passed') result.errorMessage = 'Digital signature not passed' } return result } // ALTERNATIVE LOGIN METHOD (QR-CODE) static async loginByCode(code) { let result = { success: false, errorMessage: '' } const fingerprint = await this._getFingerprint() if (this._onBeforeLogin) { this._onBeforeLogin(code, fingerprint) } if (code) { const url = this._createUrl(this._alternativeLoginUrl) try { const response = await this._post(url, { code, type: 2, fingerprint }) if (response.data.Success) { this._setTokens(response) result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { this._logger.warn('QR-code not passed') result.errorMessage = 'QR-code not passed' } return result } // ALTERNATIVE LOGIN METHOD (Open ID Connect) static async loginByOidc(options) { let result = { success: false, errorMessage: '' } options = { ...options } if (this._onBeforeLogin) { this._onBeforeLogin({ type: 'oidc', options }) } if (this._oidc) { let prompt = 'login' if (options.prompt === 'default') { prompt = '' } else if (options.prompt) { prompt = options.prompt } if (localStorage.getItem(this._needToLogInKey) === 'true') { prompt = 'login' } try { await this._oidc.signinRedirect({ state: { returnUrl: options.returnUrl ? options.returnUrl : this._webUrl }, ui_locales: options.lang, prompt: prompt, acr_values: options.acr_values, extraQueryParams: options.extraQueryParams }) } catch (e) { const failReason = e result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(`[OIDC] ${failReason}`) } } else { this._logger.warn('oidc-client not configured') result.errorMessage = 'oidc-client not configured' } return result } // OIDC FLOW static loginByOidcRedirectComplite(token) { this._setOidcToken(token) } // ALTERNATIVE LOGIN METHOD (SMS) static async loginBySMS(phone) { let result = { success: false, errorMessage: '' } const fingerprint = await this._getFingerprint() if (this._onBeforeLogin) { this._onBeforeLogin(phone, fingerprint) } if (phone) { const url = this._createUrl(this._alternativeLoginUrl) try { const response = await this._post(url, { code: phone, type: 0, fingerprint }) if (response.data.Success) { this._tempAccessToken = response.data.AccessToken result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { this._logger.warn('Login with SMS - failed') result.errorMessage = 'Login with SMS - failed' } return result } // ENTER ONE TIME PASSWORD (SMS code) static async enterOneTimeSMSPassword(tmpPass) { let result = { success: false, errorMessage: '' } if (tmpPass) { const url = this._createUrl(this._checkTempPassAltUrl) const options = { headers: { 'Authorization': `Bearer ${this._tempAccessToken}` } } try { const response = await this._post(url, { temppassword: tmpPass }, options) if (response.data.Success) { this._setTokens(response) result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { this._logger.warn('One Time password not passed') result.errorMessage = 'One time password not passed' } return result } // LOGIN BY PIN CODE (PIN-CODE) static async loginByPINCode(PINCode) { return await this._loginByBarOrPIN(PINCode, true) } // LOGIN BY BAR CODE (BAR-CODE) static async loginByBarCode(barCode) { return await this._loginByBarOrPIN(barCode, false) } static async _loginByBarOrPIN(code, isPIN) { let result = { success: false, errorMessage: '' } const fingerprint = await this._getFingerprint() if (this._onBeforeLogin) { this._onBeforeLogin(code, fingerprint) } if (code) { const url = this._createUrl(isPIN ? this._loginByPINCodeUrl : this._loginByBarCodeUrl) try { const response = await this._post(url, { code, fingerprint }) if (response.data.Success) { this._setTokens(response) result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { let msg = isPIN ? 'PIN-code not passed' : 'Bar-code not passed' this._logger.warn(msg) result.errorMessage = msg } return result } // LOGOUT static async logout(options) { var oidcToken = this._oidc ? this._getOidcToken() : null this.clearTokens() if (oidcToken) { options = { ...options } // выход oidc if (!options.skipSigninSilent) { let usr try { // для выхода необходим токен oidc usr = await this._oidc.signinSilent({ state: 'some data' }) } catch (e) { return } if (!usr) { return } } else { try { localStorage.setItem(this._needToLogInKey, true) this._oidc.signinRedirect({ state: { returnUrl: options.returnUrl ? options.returnUrl : this._webUrl }, ui_locales: options.lang, prompt: 'login' }) } catch (e) { return } return } try { // выходим await this._oidc.signoutRedirect({ state: { returnUrl: options.returnUrl ? options.returnUrl : this._webUrl } }) } catch (e) { return } // если oidc логаут отработал, то возвращаем ошибку для предотвращения дальнейшего выполнения кода выхода проекта throw new Error('oidc logout redirect') } else { const url = this._createUrl(this._logoutUrl) try { await this._post(url) } catch (e) { return } } } // CLEAR ALL TOKENS BEFORE LOGOUT static clearTokens() { const tokenIsValid = this._validateToken(5) if (tokenIsValid) { // освобождаем лицензию на сервере const url = this._createUrl(this._closeSessionUrl) const accessToken = this._getAccessToken() this._post(url, undefined, { headers: { 'Authorization': `Bearer ${accessToken}` } }) } if (!this._useCookieForRefreshToken) { if (this._isCookie()) { this._setCookie(this._refreshTokenKey, '') } else { sessionStorage.removeItem(this._refreshTokenKey) } } sessionStorage.removeItem(this._tokenClientTimeKey) sessionStorage.removeItem(this._accessTokenKey) sessionStorage.removeItem(this._oidcTokenKey) } // GET DELEGATED USERS static async getDelegatedUsers(userId) { if (userId) { const token = await this.getToken() const url = this._createUrl(this._delegatedUsersUrl) try { const response = await this._post(url, userId, { headers: { 'Authorization': `Bearer ${token}` } }) return response.data } catch (e) { if (this._onError) { this._onError(e) } this._logger.error(e.message) return null } } else { this._logger.warn('User id not passed') return null } } // GET DELEGATED RIGHTS static async getDelegatedRights() { const token = await this.getToken() const url = this._createUrl(this._delegatedRightsUrl) try { const response = await this._axiosInstance({ url, method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }) return response.data } catch (e) { if (this._onError) { this._onError(e) } this._logger.error(e.message) return null } } // APPLY DELEGATED RIGHTS FROM OTHER USERS static async applyDelegatedRights(userId) { let result = { success: false, errorMessage: '' } if (userId) { const token = await this.getToken() const url = this._createUrl(this._applyDelegationUrl) try { const response = await this._post(url, userId, { headers: { 'Authorization': `Bearer ${token}` } }) if (response.data.Success) { this._setTokens(response) result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } } else { this._logger.warn('User id not passed') result.errorMessage = 'User id not passed' } return result } // ADD DELEGATE USER static async addDelegateUser(params) { const result = { success: false, errorMessage: '' } const token = await this.getToken() const url = this._createUrl(this._addDelegationUrl) try { const response = await this._post(url, params, { headers: { 'Authorization': `Bearer ${token}` } }) if (response.data.Success) { result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } return result } // EDIT DELEGATION static async editDelegation(params) { const result = { success: false, errorMessage: '' } const token = await this.getToken() const url = this._createUrl(this._editDelegationUrl) try { const response = await this._post(url, params, { headers: { 'Authorization': `Bearer ${token}` } }) if (response.data.Success) { result.success = true } else { const failReason = response.data.FailReason result.errorMessage = this._parseErrorMessage(failReason) this._logger.warn(result.errorMessage) } } catch (e) { this._errorHandler(e, result) } return result } // GETTING USER DATA FROM ACCESS TOKEN static getUserData() { try { const accessToken = this._getAccessToken() if (accessToken) { const userInfo = jwtDecode(accessToken) const user = { id: userInfo.unique_name, login: userInfo.nameid } const usedKeys = ['unique_name', 'nameid'] for (let key in userInfo) { /* eslint-disable no-prototype-builtins */ if (!userInfo.hasOwnProperty(key) || usedKeys.includes(key)) { continue } const value = userInfo[key] key = key.replace('____', '') user[key] = value } return user } else { return null } } catch (e) { if (this._onError) { this._onError(e) } this._logger.warn(e.message) this.clearTokens() } } static async getTokenForServiceRequest() { return await this._getToken() } // GETTING ACCESS TOKEN WITH VALIDATION AND REFRESHING static async getToken() { if (this._closeSessionAutomatically) { const that = this if (this._closeSessionAutomaticallyTimer) { clearTimeout(this._closeSessionAutomaticallyTimer) } this._closeSessionAutomaticallyTimer = setTimeout(() => { that.logout({returnUrl: window.location.pathname}).then(() => { if (this._afterSignOutAction) { this._afterSignOutAction() } }) }, 15 * 60000) // 15 минут } return await this._getToken() } static async _getToken() { if (this.signinSilentInProcess) { await this._awaitSigninSilentProcess(() => this.signinSilentInProcess) } if (this.signinSilentErrorTimeout) { var that = this clearTimeout(this.resetTimeout) this.resetTimeout = setTimeout(() => { that.signinSilentErrorTimeout = false }, 3000) return } let token = '' if (!sessionStorage.getItem(this._needConfirmTokenKey)) { token = this._getAccessToken() } // если нет локального токена проверяем токены oidc if (!token && this._oidc) { let oidcToken = this._getOidcToken() if (!oidcToken || oidcToken === '*') { // если токена нет, то SSO let result try { this.signinSilentInProcess = true result = await this._oidc.signinSilent({ state: 'some data' }) this.signinSilentInProcess = false } catch (e) { this._logger.warn(e) if (e?.message?.includes('IFrame timed out without a response')) { this.signinSilentErrorTimeout = true } this.signinSilentInProcess = false //если есть рефреш токен, то продолжаем обработку if (!this._getRefreshToken()) { return } } oidcToken = result ? result.access_token : null if (oidcToken && this._returnOidcToken) { this._setOidcToken(oidcToken) return oidcToken } } // обмениваем токен oidc на внутренний if (oidcToken) { // возвращаем оригинальный токен oidc if (this._returnOidcToken) { if (this._validateOidcToken(oidcToken)) { return oidcToken } else { let result try { this.signinSilentInProcess = true result = await this._oidc.signinSilent({ state: 'some data' }) this.signinSilentInProcess = false oidcToken = result ? result.access_token : null this._setOidcToken(oidcToken) return oidcToken } catch (e) { this._logger.warn(e) if (e?.message?.includes('IFrame timed out without a response')) { this.signinSilentErrorTimeout = true } this.signinSilentInProcess = false return } } } var result = await this._exchangeOidcToken(oidcToken) if (result) { // отмечаем, что вход был с oidc для корректного выхода this._setOidcToken('*') } else { this.clearTokens() } return result } } // если включен признак блокировки запроса на рефреш и рефреш уже идёт if (this._needLockRefreshToken && this._refreshTokenLocked) { this._logger.warn('Refresh token in process') // если токен ещё валидный без дополнительного времени, то можно вернуть его if (this._validateToken(2)) { this._logger.warn('Refresh token in process, but returned alive token') return this._getAccessToken() } else { // если не валидный, то приостанавливаем выполнение метода максимум на 15 секунд this._logger.warn('Refresh token start waiting') for (var i = 0; i < 15; i++) { await new Promise(resolve => { setTimeout(() => resolve(), 1000) }) // если токен разблокировался, то продолжаем работу if (!this._refreshTokenLocked) { break } } this._logger.warn('Refresh token end waiting') // если токен не разблокировался за 15 секунд, то выдаем сообщение и возвращаем пустой токен if (this._refreshTokenLocked) { this._logger.warn('Refresh token locked') return } this._logger.warn('Refresh token unlocked') } } this._logger.warn('Refresh token start valid') const tokenIsValid = this._validateToken(5) this._logger.warn('Refresh token end valid') if (!tokenIsValid) { this._logger.warn('Refresh token not valid!') var refreshToken = '' if (!this._useCookieForRefreshToken) { refreshToken = this._getRefreshToken() if (!refreshToken) { this._logger.warn('Refresh token not found!') this.clearTokens() return } } this._logger.warn('Refresh token start refreshing') if (this._needLockRefreshToken) { this._logger.warn('Refresh token set locked true') this._refreshTokenLocked = true } await this._refreshToken(refreshToken) this._logger.warn('Refresh token end refreshing') } this._logger.warn('Refresh token is valid') const accessToken = this._getAccessToken() return accessToken } static _awaitSigninSilentProcess(inProcess) { return new Promise((resolve) => { const timer = setInterval(() => { if (!inProcess()) { clearInterval(timer) resolve() } }, 250) }) } ////////////////////////////////// /// INTERNAL SERVICE FUNCTIONS /// ////////////////////////////////// // URL CONSTRUCTOR static _createUrl(url) { try { if (this._baseUrl) { const urlRegExp = /(http|https):\/\/\w+/ const isAbsolute = urlRegExp.test(this._baseUrl) const origin = isAbsolute ? '' : window.location.origin const startSeparator = !isAbsolute && !this._baseUrl.startsWith('/') ? '/' : '' const endSeparator = this._baseUrl.endsWith('/') ? '' : '/' const urlInstance = new URL(url, origin + startSeparator + this._baseUrl + endSeparator) return urlInstance.href } else { this._logger.warn('Base URL not passed. Call auth.config({ baseUrl: \'http(s)://baseUrl\' })') return null } } catch (e) { if (this._onError) { this._onError(e) } this._logger.warn('Invalid base URL: expecting string that representing absolute or relative URL.') return null } } // exchange oidc token for inner token static async _exchangeOidcToken(oidcToken) { const fingerprint = await this._getFingerprint() if (this._onBeforeLogin) { this._onBeforeLogin({ type: 'oidc', sso: true }) } const url = this._createUrl(this._alternativeLoginUrl) let response try { response = await this._post(url, { code: oidcToken, type: 9, fingerprint }) } catch (e) { return } if (response.data.Success) { this._setTokens(response) return this._getAccessToken() } else { const failReason = response.data.FailReason this._logger.warn(failReason) return } } // CHECKING TOKEN IS VALID static _validateToken(minutes = 5) { let isValid = false const accessToken = this._getAccessToken() try { if (accessToken) { isValid = this._checkTokenExpired(accessToken, minutes) } } catch (e) { this._logger.warn(e.message) } return isValid } static _checkTokenExpired(token, minutes) { const jwtToken = jwtDecode(token) let date = new Date() date.setMinutes(date.getMinutes() + minutes) let tokenTime = jwtToken.exp * 1000 if (this._validateTokenBasedOnClientTime) { let tokenClientTime = this._tokenClientTime() let dateClientTime = new Date(+tokenClientTime) tokenTime = dateClientTime.setMilliseconds((jwtToken.exp - jwtToken.iat) * 1000) } let isValid = date.getTime() < tokenTime if (!isValid) { this._logger.warn(`Token time has expired: ${date.getTime()} > ${tokenTime}`) } this._logger.warn(`Token is valid = ${isValid}`) return isValid } static _validateOidcToken(token) { return this._checkTokenExpired(token, 5) } static _tokenClientTime() { return sessionStorage.getItem(this._tokenClientTimeKey) } // GETTING ACCESS TOKEN static _getAccessToken() { return sessionStorage.getItem(this._accessTokenKey) } // GETTING ACCESS TOKEN static _getOidcToken() { return sessionStorage.getItem(this._oidcTokenKey) } // DEFINE REFRESH TOKEN STORE static _isCookie() { return !!this._getCookie(this._refreshTokenKey) } // GETTING REFRESH TOKEN static _getRefreshToken() { if (this._isCookie()) { let cookieValue = this._getCookie(this._refreshTokenKey) if (!cookieValue) { this._logger.warn('Token not found in cookie') } return cookieValue } let sessionStorageValue = sessionStorage.getItem(this._refreshTokenKey) if (!sessionStorageValue) { this._logger.warn('Token not found in sessionStorage') } return sessionStorageValue } // QUEUE FOR REFRESH REQUESTS static _refreshQueue = [] // CLEAR QUEUE static _clearRefreshQueue() { if (!this._refreshQueue.length) { return } this._refreshQueue.pop() this._clearRefreshQueue() } // REFRESH ACCESS TOKEN WHEN IT'S NOT VALID static async _refreshToken(refreshToken) { if (this._refreshQueue.length) { return new Promise(resolve => { this._refreshQueue.push(() => resolve()) }) } this._refreshQueue.push(() => Promise.resolve()) this._logger.warn('Refresh token get fingerprint') const fingerprint = await this._getFingerprint() if (this._onBeforeRefresh) { this._onBeforeRefresh(refreshToken, fingerprint) } this._logger.warn('Refresh token create url') const url = this._createUrl(this._refreshUrl) try { this._logger.warn('Refresh token start send request') var params = { refreshToken, fingerprint } params.accessToken = this._getAccessToken() const result = await this._post(url, params) this._logger.warn('Refresh token end send request') if (result && result.data.AccessToken) { this._logger.warn('Refresh token success result') this._setTokens(result, this._isCookie()) this._clearRefreshQueue() } else { this._logger.warn('Refresh token error result') if (this._onError) { this._onError(result) } this._logger.warn(result.data.FailReason) } } catch (e) { this._clearRefreshQueue() if (this._onError) { this._onError(e) } throw Error('Refresh error: ' + e) } finally { this._logger.warn('Refresh token set locked false') this._refreshTokenLocked = false if (this._onAfterRefresh) { this._onAfterRefresh() } } } // GET TOKENS FROM SERVER RESPONSE AND WRITING TO BROWSER STORE static _setTokens(response, rememberMe) { this.signinSilentErrorTimeout = false const accessToken = response.data.AccessToken const refreshToken = response.data.RefreshToken if (rememberMe) { this._setCookie(this._refreshTokenKey, this._useServerRememberMe ? '*' : refreshToken, { secure: true, samesite: 'lax' }) } else if (!this._useCookieForRefreshToken) { sessionStorage.setItem(this._refreshTokenKey, refreshToken) } sessionStorage.setItem(this._tokenClientTimeKey, new Date().getTime()) sessionStorage.setItem(this._accessTokenKey, accessToken) localStorage.removeItem(this._needToLogInKey) } static _setCookie(name, value, options = {}) { options = { path: '/', ...options } if (options.expires instanceof Date) { options.expires = options.expires.toUTCString() } let updatedCookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) for (let optionKey in options) { updatedCookie += '; ' + optionKey let optionValue = options[optionKey] if (optionValue !== true) { updatedCookie += '=' + optionValue } } document.cookie = updatedCookie } static _getCookie(name) { let matches = document.cookie.match(new RegExp( // eslint-disable-next-line '(?:^|; )' + (name || '').replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)' )) return matches ? decodeURIComponent(matches[1]) : undefined } static _setOidcToken(oidcToken) { sessionStorage.setItem(this._oidcTokenKey, oidcToken) } // CREATE CLIENT FINGERPRINT static async _getFingerprint() { return this._fingerprint || 'ed3b5d8c811f85417ecaae07505e6cfc' // 'static fingerprint' (MD5 hash) // TODO: Необходимо попробовать другие способы, т.к. код ниже не гарантирует уникальность ключа // return new Promise((resolve, reject) => { // async function getHash() { // try { // const components = await Fingerprint2.getPromise() // const values = components.map(component => component.value) // return String(Fingerprint2.x64hash128(values.join(''), 31)) // } catch (e) { // reject(e) // } // } // if (window.requestIdleCallback) { // requestIdleCallback(async () => resolve(await getHash())) // } else { // setTimeout(async () => resolve(await getHash()), 500) // } // }) } // PARSE FAIL REASON static _parseErrorMessage(message) { const searchRegExp = /FAILREASON/gmi if (searchRegExp.test(message)) { const trimMessage = message.replace(/[,:]/g, '') const parsedMessage = trimMessage.split('""') return parsedMessage[1] } return message } // ERROR HANDLER static _errorHandler(error, result) { if (this._onError) { this._onError(error) } if (error.response && error.response.data.FailReason) { const reason = error.response.data.FailReason result.errorMessage = this._parseErrorMessage(reason) } else { result.errorMessage = error.message } this._logger.warn(result.errorMessage) } static async _post(url, data, config) { if (config) { config.withCredentials = true } else { config = { withCredentials: true } } return await this._axiosInstance.post(url, data, config) } }