UNPKG

alfresco-js-api

Version:

JavaScript client library for the Alfresco REST API

645 lines (535 loc) 21.1 kB
'use strict'; var AlfrescoApiClient = require('./alfrescoApiClient'); var Emitter = require('event-emitter'); var rs = require('jsrsasign'); class Oauth2Auth extends AlfrescoApiClient { /** * @param {Object} config */ constructor(config) { super(); this.config = config; this.init = false; if (this.config.oauth2) { if (this.config.oauth2.host === undefined || this.config.oauth2.host === null) { throw 'Missing the required oauth2 host parameter'; } if (this.config.oauth2.clientId === undefined || this.config.oauth2.clientId === null) { throw 'Missing the required oauth2 clientId parameter'; } if (this.config.oauth2.scope === undefined || this.config.oauth2.scope === null) { throw 'Missing the required oauth2 scope parameter'; } if (this.config.oauth2.secret === undefined || this.config.oauth2.secret === null) { this.config.oauth2.secret = ''; } if ((this.config.oauth2.redirectUri === undefined || this.config.oauth2.redirectUri === null) && this.config.oauth2.implicitFlow) { throw 'Missing redirectUri required parameter'; } if (!this.config.oauth2.refreshTokenTimeout) { this.config.oauth2.refreshTokenTimeout = 40000; } this.basePath = this.config.oauth2.host; //Auth Call this.authentications = { 'basicAuth': {type: 'oauth2', accessToken: ''} }; this.host = this.config.oauth2.host; if (this.config.oauth2.implicitFlow) { this.initOauth();// jshint ignore:line } if (this.config.accessToken) { this.setTicket(this.config.accessToken); } } Emitter.call(this); } initOauth() { return Promise.resolve() .then(() => { return this.discoveryUrls(); }) .then(() => { return this.loadJwks(); }) .then(() => { return this.checkFragment(); }); } discoveryUrls() { return new Promise((resolve, reject) => { let discoveryStore = this.storage.getItem('discovery'); if (discoveryStore) { this.discovery = JSON.parse(discoveryStore); } if (!this.discovery) { let postBody = {}, pathParams = {}, queryParams = {}, formParams = {}, headerParams = {}; let authNames = []; let contentTypes = ['application/json']; let accepts = ['application/json']; let url = '.well-known/openid-configuration'; this.callApi( url, 'GET', pathParams, queryParams, headerParams, formParams, postBody, authNames, contentTypes, accepts, {} ).then((discovery) => { this.discovery = {}; this.discovery.loginUrl = discovery.authorization_endpoint; this.discovery.logoutUrl = discovery.end_session_endpoint; this.discovery.grantTypesSupported = discovery.grant_types_supported; this.discovery.issuer = discovery.issuer; this.discovery.tokenEndpoint = discovery.token_endpoint; this.discovery.userinfoEndpoint = discovery.userinfo_endpoint; this.discovery.jwksUri = discovery.jwks_uri; this.discovery.sessionCheckIFrameUrl = discovery.check_session_iframe; this.emit('discovery', this.discovery); this.storage.setItem('discovery', JSON.stringify(this.discovery)); resolve(discovery); }, (error) => { console.log(error); reject(error.error); }); } else { this.emit('discovery', this.discovery); resolve(this.discovery); } }); } loadJwks() { return new Promise((resolve, reject) => { let jwksStore = this.storage.getItem('jwks'); if (jwksStore) { this.jwks = JSON.parse(jwksStore); } if (this.discovery.jwksUri) { if (!this.jwks) { let postBody = {}, pathParams = {}, queryParams = {}, formParams = {}, headerParams = {}; let authNames = []; let contentTypes = ['application/json']; let accepts = ['application/json']; this.callCustomApi( this.discovery.jwksUri, 'GET', pathParams, queryParams, headerParams, formParams, postBody, authNames, contentTypes, accepts, {} ).then((jwks) => { this.jwks = jwks; this.emit('jwks', jwks); this.storage.setItem('jwks', JSON.stringify(jwks)); resolve(jwks); }, (error) => { reject(error.error); }); } else { this.emit('jwks', this.jwks); resolve(this.jwks); } } else { reject('jwks error'); } }); } checkFragment(externalHash) {// jshint ignore:line return new Promise((resolve, reject) => { this.hashFragmentParams = this.getHashFragmentParams(externalHash); if (this.hashFragmentParams) { let accessToken = this.hashFragmentParams.access_token; let idToken = this.hashFragmentParams.id_token; let sessionState = this.hashFragmentParams.session_state; let expiresIn = this.hashFragmentParams.expires_in; if (!sessionState) { reject('session state not present'); } this.processJWTToken(idToken, accessToken).then((jwt) => { if (jwt) { this.storeIdToken(idToken, jwt.payload.exp); this.storeAccessToken(accessToken, expiresIn); this.authentications.basicAuth.username = jwt.payload.preferred_username; this.saveUsername(jwt.payload.preferred_username); this.silentRefresh(); resolve(accessToken); } }, (error) => { reject('Validation JWT error' + error); }); } else { if (this.isValidAccessToken()) { let accessToken = this.storage.getItem('access_token'); this.setToken(accessToken, null); resolve(accessToken); } else if (this.config.oauth2.silentLogin) { this.implicitLogin(); } } }); } processJWTToken(jwt) { return new Promise((resolve, reject) => { if (jwt) { const header = rs.jws.JWS.readSafeJSONString(rs.b64utoutf8(jwt.split('.')[0])); const payload = rs.jws.JWS.readSafeJSONString(rs.b64utoutf8(jwt.split('.')[1])); const savedNonce = this.storage.getItem('nonce'); if (!payload.sub) { reject('Missing sub in JWT'); } if (payload.nonce !== savedNonce) { reject('Failing nonce JWT is not corrisponding' + payload.nonce); } if (this.jwks) { let validObj = this.validateJWKS(this.jwks, jwt, payload, header); if (validObj) { resolve(validObj); } else { reject('Invalid JWT'); } } } }); } validateJWKS(jwks, jwt, payload, header) { let keyObj = rs.KEYUTIL.getKey(jwks.keys[0]); let isValid = rs.jws.JWS.verifyJWT(jwt, keyObj, { alg: [header.alg], iss: [this.config.oauth2.host], aud: [this.config.oauth2.clientId] }); if (isValid) { return { idToken: jwt, payload: payload, header: header }; } } storeIdToken(idToken, exp) { this.storage.setItem('id_token', idToken); this.storage.setItem('id_token_expires_at', Number(exp * 1000).toString()); this.storage.setItem('id_token_stored_at', Date.now().toString()); } storeAccessToken(accessToken, expiresIn) { this.storage.setItem('access_token', accessToken); const expiresInMilliSeconds = expiresIn * 1000; const now = new Date(); const expiresAt = now.getTime() + expiresInMilliSeconds; this.storage.setItem('access_token_expires_in', expiresAt); this.storage.setItem('access_token_stored_at', Date.now().toString()); this.setToken(accessToken, null); } saveUsername(username) { if (this.storage.supportsStorage()) { this.storage.setItem('USERNAME', username); } } implicitLogin() { if (!this.isValidToken() || !this.isValidAccessToken()) { if (this.discovery && this.discovery.loginUrl) { this.redirectLogin(); } else { this.on('discovery', () => { this.redirectLogin(); }); } } else { let accessToken = this.storage.getItem('access_token'); this.setToken(accessToken, null); } } isValidToken() { var validToken = false; if (this.getIdToken()) { var expiresAt = this.storage.getItem('id_token_expires_at'), now = new Date(); if (expiresAt && parseInt(expiresAt, 10) >= now.getTime()) { validToken = true; } } return validToken; } isValidAccessToken() { var validAccessToken = false; if (this.getAccessToken()) { const expiresAt = this.storage.getItem('access_token_expires_in'); const now = new Date(); if (expiresAt && parseInt(expiresAt, 10) >= now.getTime()) { validAccessToken = true; } } return validAccessToken; } getIdToken() { return this.storage.getItem('id_token'); } getAccessToken() { return this.storage.getItem('access_token'); } redirectLogin() { if (this.config.oauth2.implicitFlow && typeof window !== 'undefined') { let href = this.composeImplicitLoginUrl(); window.location.href = href; this.emit('implicit_redirect', href); } } genNonce() { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 40; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } composeImplicitLoginUrl() { var nonce = this.genNonce(); this.storage.setItem('nonce', nonce); var separation = this.discovery.loginUrl.indexOf('?') > -1 ? '&' : '?'; return this.discovery.loginUrl + separation + 'client_id=' + encodeURIComponent(this.config.oauth2.clientId) + '&redirect_uri=' + encodeURIComponent(this.config.oauth2.redirectUri) + '&scope=' + encodeURIComponent(this.config.oauth2.scope) + '&response_type=' + encodeURIComponent('id_token token') + '&nonce=' + encodeURIComponent(nonce); } getHashFragmentParams(externalHash) { var hashFragmentParams = null; if (typeof window !== 'undefined') { let hash; if (!externalHash) { hash = decodeURIComponent(window.location.hash); } else { hash = decodeURIComponent(externalHash); } if (hash.indexOf('#') === 0) { const questionMarkPosition = hash.indexOf('?'); if (questionMarkPosition > -1) { hash = hash.substr(questionMarkPosition + 1); } else { hash = hash.substr(1); } hashFragmentParams = this.parseQueryString(hash); } } return hashFragmentParams; } parseQueryString(queryString) { const data = {}; var pairs, pair, separatorIndex, escapedKey, escapedValue, key, value; if (queryString !== null) { pairs = queryString.split('&'); for (var i = 0; i < pairs.length; i++) { pair = pairs[i]; separatorIndex = pair.indexOf('='); if (separatorIndex === -1) { escapedKey = pair; escapedValue = null; } else { escapedKey = pair.substr(0, separatorIndex); escapedValue = pair.substr(separatorIndex + 1); } key = decodeURIComponent(escapedKey); value = decodeURIComponent(escapedValue); if (key.substr(0, 1) === '/') { key = key.substr(1); } data[key] = value; } } return data; } silentRefresh() { if (typeof document === 'undefined') { throw new Error('Silent refresh supported only on browsers'); } setTimeout(() => { this.destroyIframe(); this.createIframe(); }, this.config.oauth2.refreshTokenTimeout); } createIframe() { const iframe = document.createElement('iframe'); iframe.id = 'silent_refresh_token_iframe'; let loginUrl = this.composeImplicitLoginUrl(); iframe.setAttribute('src', loginUrl); iframe.style.display = 'none'; document.body.appendChild(iframe); this.iFameHashListner = () => { let hash = document.getElementById('silent_refresh_token_iframe').contentWindow.location.hash; this.checkFragment(hash); }; iframe.addEventListener('load', this.iFameHashListner); } destroyIframe() { const iframe = document.getElementById('silent_refresh_token_iframe'); if (iframe) { iframe.removeEventListener('load', this.iFameHashListner); document.body.removeChild(iframe); } } /** * login Alfresco API * @param {String} username: // Username to login * @param {String} password: // Password to login * * @returns {Promise} A promise that returns {new authentication token} if resolved and {error} if rejected. * */ login(username, password) { this.promise = new Promise((resolve, reject) => { var postBody = {}, pathParams = {}, queryParams = {}, formParams = {}; var auth = 'Basic ' + new Buffer(this.config.oauth2.clientId + ':' + this.config.oauth2.secret).toString('base64'); var headerParams = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': auth }; formParams = { username: username, password: password, grant_type: 'password', client_id: this.config.oauth2.clientId }; var authNames = []; var contentTypes = ['application/x-www-form-urlencoded']; var accepts = ['application/json']; var url = this.config.oauth2.authPath || '/oauth/token'; this.callApi( url, 'POST', pathParams, queryParams, headerParams, formParams, postBody, authNames, contentTypes, accepts, {} ).then( (data) => { this.saveUsername(username); this.setToken(data.access_token, data.refresh_token); resolve(data); }, (error) => { if (error.error.status === 401) { this.promise.emit('unauthorized'); } this.promise.emit('error'); reject(error.error); }); Emitter(this.promise); // jshint ignore:line }); return this.promise; } /** * Refresh the Token * */ refreshToken() { var postBody = {}, pathParams = {}, queryParams = {}, formParams = {}; var auth = 'Basic ' + new Buffer(this.config.oauth2.clientId + ':' + this.config.oauth2.secret).toString('base64'); var headerParams = { 'Content-Type': 'application/x-www-form-urlencoded', 'Cache-Control': 'no-cache', 'Authorization': auth }; queryParams = { refresh_token: this.authentications.basicAuth.refreshToken, grant_type: 'refresh_token' }; var authNames = []; var contentTypes = ['application/x-www-form-urlencoded']; var accepts = ['application/json']; this.promise = new Promise((resolve, reject) => { this.callApi( '/oauth/token', 'POST', pathParams, queryParams, headerParams, formParams, postBody, authNames, contentTypes, accepts, {} ).then( (data) => { this.setToken(data.access_token, data.refresh_token); resolve(data); }, (error) => { if (error.error.status === 401) { this.promise.emit('unauthorized'); } this.promise.emit('error'); reject(error.error); }); }); Emitter(this.promise); // jshint ignore:line return this.promise; } /** * Set the current Token * * @param {String} Token * @param {String} refreshToken * */ setToken(token, refreshToken) { this.authentications.basicAuth.accessToken = token; this.authentications.basicAuth.refreshToken = refreshToken; this.authentications.basicAuth.password = null; this.token = token; this.emit('token_issued'); } /** * Get the current Token * * */ getToken() { return this.token; } /** * return the Authentication * * @returns {Object} authentications * */ getAuthentication() { return this.authentications; } /** * Change the Host * * @param {String} host * */ changeHost(host) { this.config.hostOauth2 = host; } /** * If the client is logged in retun true * * @returns {Boolean} is logged in */ isLoggedIn() { return !!this.authentications.basicAuth.accessToken; } /** * Logout **/ logOut() { const id_token = this.getIdToken(); this.cleanStorage(); this.setToken(null, null); var separation = this.discovery.logoutUrl.indexOf('?') > -1 ? '&' : '?'; let redirectLogout = this.config.oauth2.redirectUriLogout || this.config.oauth2.redirectUri; var logoutUrl = this.discovery.logoutUrl + separation + 'post_logout_redirect_uri=' + encodeURIComponent(redirectLogout) + '&id_token_hint=' + encodeURIComponent(id_token); if (this.config.oauth2.implicitFlow && typeof window !== 'undefined') { window.location.href = logoutUrl; } } cleanStorage() { this.storage.removeItem('access_token'); this.storage.removeItem('access_token_expires_in'); this.storage.removeItem('access_token_stored_at'); this.storage.removeItem('id_token'); this.storage.removeItem('id_token'); this.storage.removeItem('id_token_claims_obj'); this.storage.removeItem('id_token_expires_at'); this.storage.removeItem('id_token_stored_at'); this.storage.removeItem('nonce'); this.storage.removeItem('jwks'); this.storage.removeItem('discovery'); } } Emitter(Oauth2Auth.prototype); // jshint ignore:line module.exports = Oauth2Auth;