@mo-platform/auth
Version:
Mo authentication library
345 lines (297 loc) • 9.31 kB
JavaScript
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 };