UNPKG

@mo-platform/auth

Version:

Mo authentication library

345 lines (297 loc) 9.31 kB
class EventEmitter { constructor () { this.events = {}; } _getEventListByName (eventName) { if (typeof this.events[eventName] === 'undefined') { this.events[eventName] = new Set(); } return this.events[eventName] } on (eventName, fn) { this._getEventListByName(eventName).add(fn); } once (eventName, fn) { const self = this; const onceFn = function (...args) { self.removeListener(eventName, onceFn); fn.apply(self, args); }; this.on(eventName, onceFn); } emit (eventName, ...args) { this._getEventListByName(eventName).forEach(function (fn) { fn.apply(this, args); }.bind(this)); } removeListener (eventName, fn) { this._getEventListByName(eventName).delete(fn); } } const b64DecodeUnicode = (str) => { return decodeURIComponent( atob(str).replace(/(.)/g, function(m, p) { var code = p.charCodeAt(0).toString(16).toUpperCase(); if (code.length < 2) { code = "0" + code; } return "%" + code }) ) }; const base64_url_decode = (str) => { var output = str.replace(/-/g, "+").replace(/_/g, "/"); switch (output.length % 4) { case 0: break case 2: output += "=="; break case 3: output += "="; break default: throw "Illegal base64url string!" } try { return b64DecodeUnicode(output) } catch (err) { return atob(output) } }; const jwtDecode = (token, options) => { if (typeof token !== "string") { throw new Error("Invalid token specified") } options = options || {}; var pos = options.header === true ? 0 : 1; try { const [ headers, payload, sig ] = token.split("."); if(payload && sig) return JSON.parse(base64_url_decode(token.split(".")[pos])) else throw new Error("malformed token") } catch (e) { throw new Error("Invalid token specified: " + e.message) } }; /* eslint-disable no-async-promise-executor */ class Authentication { constructor (opts = {}) { const { authUrl, clientId, storage, loginUrl, logoutUrl, userFetchUrl, accessCheckUrl, refreshTokenUrl } = opts; this.authUrl = authUrl || 'https://mo-auth.fagbokforlaget.no'; this.currentUser = undefined; this.token = undefined; this.clientId = clientId; this.storage = storage || window.localStorage; this.loginUrl = loginUrl || this.authUrl + '/_auth/login'; this.logoutUrl = logoutUrl; this.userFetchUrl = userFetchUrl || this.authUrl + '/_auth/user'; this.accessCheckUrl = accessCheckUrl || this.authUrl + '/_auth/access'; this.refreshTokenUrl = refreshTokenUrl || ''; this.EventEmitter = new EventEmitter(); } _loginUrl (redirectUrl, scope = undefined, namespaceId = undefined, namespaceConfigId = undefined) { if (!this.loginUrl.includes('?')) { this.loginUrl += '?'; } this.loginUrl += '&client_id=' + (this.clientId || 'generic') + '&redirect_url=' + encodeURIComponent(redirectUrl) + '&scope=' + (scope || 'dbok'); // Both namespaceId and namespaceConfigId are required to perform access check on server if(namespaceId && namespaceConfigId) this.loginUrl += `&namespace_id=${namespaceId}&namespace_config_id=${namespaceConfigId}`; return this.loginUrl; } _parseQueryString (loc) { const pl = /\+/g; const search = /([^&=]+)=?([^&]*)/g; const decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')) }; const urlParams = {}; let query = loc.substring(1); if (/\?/.test(query)) { query = query.split('?')[1]; } while (1) { const match = search.exec(query); if (!match) { break } urlParams[decode(match[1])] = decode(match[2]); } return urlParams } authorize (obj = {}) { const { redirectUrl, scope, namespaceConfigId, namespaceId } = obj; window.location = this._loginUrl(redirectUrl || window.location, scope, namespaceId, namespaceConfigId); } async refreshTokenTimer (refreshTime = 15 * 60000) { this.silentRefresh = setInterval(async () => { return new Promise(async (resolve, reject) => { this.getRefreshToken().then((token) => { resolve(token); }).catch(e => { reject(e); }); }) }, refreshTime); } async getRefreshToken () { return new Promise(async (resolve, reject) => { let response, resp; try { response = await fetch(this.refreshTokenUrl, { method: 'POST', credentials: 'include' }); resp = await response.json(); if (resp && resp.success) { this.storage.setItem('token', resp.idToken); this.token = resp.idToken; this.EventEmitter.emit('accessTokenUpdated', resp.idToken); resolve(resp.idToken); } else { throw new Error('Failed to refresh access token') } } catch (err) { reject(err); } }); } stopRefreshTimer () { clearInterval(this.silentRefresh); } getUser () { const storeUser = this.storage.getItem('user'); if (this.currentUser) { return this.currentUser } if (storeUser) { return JSON.parse(storeUser) } return undefined } async jwtExpiryCheck(token) { // try { let decodedToken = jwtDecode(token); if (decodedToken.exp * 1000 < Date.now()) { throw new Error("Token Expired") } // } // catch(err) { // await this.getRefreshToken() // this.stopRefreshTimer() // if (decodedToken.exp && decodedToken.iat) { // this.refreshTokenTimer((decodedToken.exp - decodedToken.iat) * 1000) // } else { // this.refreshTokenTimer() // } // } } checkToken (loc = window.location.search) { const params = this._parseQueryString(loc); const self = this; self.token = params.token || params.access_token || this.storage.getItem('token') || undefined; return new Promise(async (resolve, reject) => { // if (self.isAuthenticated() && self.token && typeof self.token !== 'undefined') { // resolve(self.getUser()) // } if (self.token && typeof self.token !== 'undefined') { await self.jwtExpiryCheck(self.token).catch((err) => { reject(err); }); self.storage.setItem('token', self.token); self.fetchUser(this.userFetchUrl) .then((user) => { resolve(user); }) .catch((err) => { reject(err); }); } else { reject(new Error('access token not found')); } }) } checkAccess (productIds = []) { const token = this.token || this.storage.getItem('token') || undefined; const products = Array.isArray(productIds) ? productIds : [productIds]; const user = this.getUser(); return new Promise(async (resolve, reject) => { if (token && typeof token !== 'undefined') { try { await this.jwtExpiryCheck(token).catch((err) => { reject(err); }); const allowedProducts = await this.fetchAccess(this.accessCheckUrl, { productIds: products }); resolve({ success: true, user: user, products: allowedProducts }); } catch (err) { reject(err); } } else { reject(new Error('access token not found')); } }) } fetchUser (url) { const self = this; return new Promise(async (resolve, reject) => { const response = await fetch(url, { method: 'GET', mode: 'cors', headers: { 'Content-Type': 'application/json', 'X-Access-Token': self.token || this.storage.getItem('token') } }); if (response.status === 200) { const resp = await response.json(); const user = resp.user || resp.objects[0]; self.storage.setItem('user', JSON.stringify(user)); resolve(user); } else { reject(new Error('authentication failed: Invalid response')); } }) } fetchAccess (url, body) { const self = this; return new Promise(async (resolve, reject) => { const response = await fetch(url, { method: 'POST', mode: 'cors', cache: 'no-cache', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-Access-Token': self.token || this.storage.getItem('token') }, body: JSON.stringify(body) }); if (response.status === 200) { const resp = await response.json(); if (resp.success) { resolve(resp.products); } else { reject(new Error('This user does not have access to this product')); } } else if (response.status === 401) { reject(new Error('access token not found')); } else { reject(new Error('invalid response from server')); } }) } async logout (url) { this.storage.removeItem('user'); this.storage.removeItem('token'); this.stopRefreshTimer(); url = url || this.logoutUrl; if (url) window.location = url; } isAuthenticated () { const user = this.getUser(); if (user) { return true } return false } } export { Authentication as default };