@it-enterprise/jwtauthentication
Version:
JSON Web Token-Based authentication library
1,194 lines (1,117 loc) • 41.9 kB
JavaScript
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)
}
}