UNPKG

@jbrowse/plugin-authentication

Version:

JBrowse 2 Authentication

290 lines (289 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const configuration_1 = require("@jbrowse/core/configuration"); const models_1 = require("@jbrowse/core/pluggableElementTypes/models"); const util_1 = require("@jbrowse/core/util"); const mobx_state_tree_1 = require("mobx-state-tree"); const util_2 = require("./util"); const util_3 = require("../util"); function encode(uint8array) { const output = []; for (let i = 0, length = uint8array.length; i < length; i++) { output.push(String.fromCharCode(uint8array[i])); } return btoa(output.join('')); } const stateModelFactory = (configSchema) => { return models_1.InternetAccount.named('OAuthInternetAccount') .props({ type: mobx_state_tree_1.types.literal('OAuthInternetAccount'), configuration: (0, configuration_1.ConfigurationReference)(configSchema), }) .views(() => { let codeVerifier = undefined; return { get codeVerifierPKCE() { if (!codeVerifier) { const array = new Uint8Array(32); globalThis.crypto.getRandomValues(array); codeVerifier = (0, util_2.fixup)(encode(array)); } return codeVerifier; }, }; }) .views(self => ({ get authEndpoint() { return (0, configuration_1.getConf)(self, 'authEndpoint'); }, get tokenEndpoint() { return (0, configuration_1.getConf)(self, 'tokenEndpoint'); }, get needsPKCE() { return (0, configuration_1.getConf)(self, 'needsPKCE'); }, get clientId() { return (0, configuration_1.getConf)(self, 'clientId'); }, get scopes() { return (0, configuration_1.getConf)(self, 'scopes'); }, state() { return (0, configuration_1.getConf)(self, 'state'); }, get responseType() { return (0, configuration_1.getConf)(self, 'responseType'); }, get refreshTokenKey() { return `${self.internetAccountId}-refreshToken`; }, })) .actions(self => ({ storeRefreshToken(refreshToken) { localStorage.setItem(self.refreshTokenKey, refreshToken); }, removeRefreshToken() { localStorage.removeItem(self.refreshTokenKey); }, retrieveRefreshToken() { return localStorage.getItem(self.refreshTokenKey); }, async exchangeAuthorizationForAccessToken(token, redirectUri) { const params = new URLSearchParams(Object.entries({ code: token, grant_type: 'authorization_code', client_id: self.clientId, redirect_uri: redirectUri, ...(self.needsPKCE ? { code_verifier: self.codeVerifierPKCE } : {}), })); const response = await fetch(self.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString(), }); if (!response.ok) { throw new Error(await (0, util_3.getResponseError)({ response, reason: 'Failed to obtain token', })); } const data = await response.json(); return (0, util_2.processTokenResponse)(data, token => { this.storeRefreshToken(token); }); }, async exchangeRefreshForAccessToken(refreshToken) { const response = await fetch(self.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(Object.entries({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: self.clientId, })).toString(), }); if (!response.ok) { self.removeToken(); const text = await response.text(); throw new Error(await (0, util_3.getResponseError)({ response, statusText: (0, util_2.processError)(text, () => { this.removeRefreshToken(); }), })); } const data = await response.json(); return (0, util_2.processTokenResponse)(data, token => { this.storeRefreshToken(token); }); }, })) .actions(self => { let listener; let exchangedTokenPromise = undefined; return { addMessageChannel(resolve, reject) { listener = event => { this.finishOAuthWindow(event, resolve, reject); }; window.addEventListener('message', listener); }, deleteMessageChannel() { window.removeEventListener('message', listener); }, async finishOAuthWindow(event, resolve, reject) { if (event.data.name !== `JBrowseAuthWindow-${self.internetAccountId}`) { this.deleteMessageChannel(); return; } const redirectUriWithInfo = event.data.redirectUri; const fixedQueryString = redirectUriWithInfo.replace('#', '?'); const redirectUrl = new URL(fixedQueryString); const queryStringSearch = redirectUrl.search; const urlParams = new URLSearchParams(queryStringSearch); if (urlParams.has('access_token')) { const token = urlParams.get('access_token'); if (!token) { reject(new Error('Error with token endpoint')); return; } self.storeToken(token); resolve(token); return; } if (urlParams.has('code')) { const code = urlParams.get('code'); if (!code) { reject(new Error('Error with authorization endpoint')); return; } try { const token = await self.exchangeAuthorizationForAccessToken(code, redirectUrl.origin + redirectUrl.pathname); self.storeToken(token); resolve(token); return; } catch (e) { if (e instanceof Error) { reject(e); } else { reject(new Error(String(e))); } return; } } if (redirectUriWithInfo.includes('access_denied')) { reject(new Error('OAuth flow was cancelled')); return; } if (redirectUriWithInfo.includes('error')) { reject(new Error(`OAuth flow error: ${queryStringSearch}`)); return; } this.deleteMessageChannel(); }, async useEndpointForAuthorization(resolve, reject) { const redirectUri = util_1.isElectron ? 'http://localhost/auth' : window.location.origin + window.location.pathname; const data = { client_id: self.clientId, redirect_uri: redirectUri, response_type: self.responseType, token_access_type: 'offline', }; if (self.state()) { data.state = self.state(); } if (self.scopes) { data.scope = self.scopes; } if (self.needsPKCE) { data.code_challenge = await (0, util_2.generateChallenge)(self.codeVerifierPKCE); data.code_challenge_method = 'S256'; } const params = new URLSearchParams(Object.entries(data)); const url = new URL(self.authEndpoint); url.search = params.toString(); const eventName = `JBrowseAuthWindow-${self.internetAccountId}`; if (util_1.isElectron) { const { ipcRenderer } = window.require('electron'); const redirectUri = await ipcRenderer.invoke('openAuthWindow', { internetAccountId: self.internetAccountId, data, url: url.toString(), }); const eventFromDesktop = new MessageEvent('message', { data: { name: eventName, redirectUri: redirectUri }, }); this.finishOAuthWindow(eventFromDesktop, resolve, reject); } else { window.open(url, eventName, 'width=500,height=600,left=0,top=0'); } }, async getTokenFromUser(resolve, reject) { const refreshToken = self.retrieveRefreshToken(); let doUserFlow = true; if (refreshToken) { try { const token = await self.exchangeRefreshForAccessToken(refreshToken); resolve(token); doUserFlow = false; } catch (e) { console.error(e); self.removeRefreshToken(); } } if (doUserFlow) { this.addMessageChannel(resolve, reject); this.useEndpointForAuthorization(resolve, reject); } }, async validateToken(token, location) { const newInit = self.addAuthHeaderToInit({ method: 'HEAD' }, token); const response = await fetch(location.uri, newInit); if (!response.ok) { self.removeToken(); const refreshToken = self.retrieveRefreshToken(); if (refreshToken) { try { if (!exchangedTokenPromise) { exchangedTokenPromise = self.exchangeRefreshForAccessToken(refreshToken); } const newToken = await exchangedTokenPromise; exchangedTokenPromise = undefined; return newToken; } catch (err) { console.error('Token could not be refreshed', err); } } throw new Error(await (0, util_3.getResponseError)({ response, reason: 'Error validating token', })); } return token; }, }; }) .actions(self => { const superGetFetcher = self.getFetcher; return { getFetcher(loc) { const fetcher = superGetFetcher(loc); return async (input, init) => { if (loc) { await self.validateToken(await self.getToken(loc), loc); } return fetcher(input, init); }; }, }; }); }; exports.default = stateModelFactory;