UNPKG

neo4j-client-sso

Version:

Single sign-on client (frontend) library for Neo4j products

236 lines (235 loc) 11.9 kB
/* * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { getCredentialsFromAuthResult, getInitialisationParameters, retrieveRefreshTokenData, storeRefreshTokenData, temporarilyStoreUrlSearchParams } from './common'; import { AUTH_STORAGE_CODE_VERIFIER, AUTH_STORAGE_STATE, BEARER, IMPLICIT, PKCE } from './constants'; import { createStateForRequest, createNonce, createCodeVerifier, createCodeChallenge, authLog, authDebug, removeSearchParamsInBrowserHistory, assembleRequestBody, assembleRedirectUri } from './helpers'; import { defaultCodeChallengeMethod, defaultTokenGrantType, defaultRefreshGrantType, defaultSearchParamsToRemoveAfterAuthRedirect } from './settings'; const tokenRequestOptions = { method: 'post', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } }; export const authRequestForSSO = (selectedSSOProvider) => __awaiter(void 0, void 0, void 0, function* () { authLog('Initializing auth redirect request for SSO'); if (!selectedSSOProvider) { throw new Error('Could not find SSO provider'); } if (!window.isSecureContext) { throw new Error('This application is NOT executed in a secure context. SSO support is therefore disabled. Load the application in a secure context to proceed with SSO.'); } temporarilyStoreUrlSearchParams(); const oauth2Endpoint = selectedSSOProvider.auth_endpoint; if (!oauth2Endpoint) { throw new Error(`Invalid OAuth2 endpoint: "${oauth2Endpoint}"`); } authLog(`Using OAuth2 endpoint: "${oauth2Endpoint}" for idp_id: ${selectedSSOProvider.id}`); const form = document.createElement('form'); form.setAttribute('method', 'GET'); form.setAttribute('action', oauth2Endpoint); const redirectUri = assembleRedirectUri(selectedSSOProvider.id); authLog(`For auth request, assembled redirect_uri to be: ${redirectUri}`); const SSOParams = Object.assign(Object.assign({}, (selectedSSOProvider.params || {})), { redirect_uri: redirectUri }); const state = createStateForRequest(); let params = Object.assign(Object.assign({}, SSOParams), { state }); window.sessionStorage.setItem(AUTH_STORAGE_STATE, state); const SSOExtraAuthParams = selectedSSOProvider.auth_params || {}; if (SSOExtraAuthParams) { params = Object.assign(Object.assign({}, params), SSOExtraAuthParams); } authLog(`Using the following authorization parameter: ${JSON.stringify(SSOParams)}`); const SSOConfig = selectedSSOProvider.config || {}; if (SSOConfig.implicit_flow_requires_nonce) { params = Object.assign(Object.assign({}, params), { nonce: createNonce() }); authLog(`Using nonce in authorization request`); } const _submitForm = (form, params) => { for (const param in params) { const input = document.createElement('input'); input.setAttribute('type', 'hidden'); input.setAttribute('name', param); input.setAttribute('value', params[param]); form.appendChild(input); } document.body.appendChild(form); form.submit(); }; if (selectedSSOProvider.auth_flow === PKCE) { const codeChallengeMethod = SSOConfig.code_challenge_method || defaultCodeChallengeMethod; authLog(`Auth flow "PKCE", using code_challenge_method: "${codeChallengeMethod}"`); try { const codeVerifier = createCodeVerifier(codeChallengeMethod) || ''; window.sessionStorage.setItem(AUTH_STORAGE_CODE_VERIFIER, codeVerifier); const codeChallenge = yield createCodeChallenge(codeChallengeMethod, codeVerifier); params = Object.assign(Object.assign({}, params), { code_challenge_method: codeChallengeMethod, code_challenge: codeChallenge }); _submitForm(form, params); } catch (error) { // caller handles the catching, adding rethrowing to make // it clear we expect `createCodeVerifier` could throw. throw error; } } else if (selectedSSOProvider.auth_flow === IMPLICIT) { authLog('Auth flow "implicit flow"'); _submitForm(form, params); } else { throw new Error(`Auth flow "${selectedSSOProvider.auth_flow}" is not supported.`); } }); export const handleAuthFromRedirect = (SSOProviders, searchParamsToRemove) => new Promise((resolve, reject) => { const { idp_id: idpId, token_type: tokenType, access_token: accessToken, id_token: idToken, error_description: errorDescription, code, state, error } = getInitialisationParameters(); authLog('Handling auth redirect from SSO server'); removeSearchParamsInBrowserHistory(searchParamsToRemove || defaultSearchParamsToRemoveAfterAuthRedirect); if (error) { reject(new Error(`Error detected after auth redirect, aborting. Error: ${error}, Error description: ${errorDescription}`)); return; } if (!idpId) { reject(new Error('Invalid idp_id parameter, aborting')); return; } const savedState = window.sessionStorage.getItem(AUTH_STORAGE_STATE); if (state !== savedState) { reject(new Error('Invalid state parameter, aborting')); return; } window.sessionStorage.removeItem(AUTH_STORAGE_STATE); const selectedSSOProvider = SSOProviders.find(({ id }) => id === idpId); if (!selectedSSOProvider) { reject(new Error(`Couldn't find sso provider with id "${idpId}", only found ${SSOProviders.map(provider => provider.id).join(', ')}`)); } if ((tokenType || '').toLowerCase() === BEARER && accessToken) { authLog('Successfully acquired access_token in "implicit flow"'); authDebug('Implicit flow id_token', idToken); authDebug('Implicit flow access_token', accessToken); // INFO: Implicit flow has no support for refresh_token. try { const credentials = getCredentialsFromAuthResult({ access_token: accessToken, id_token: idToken }, selectedSSOProvider); resolve(credentials); } catch (err) { reject(new Error(`Failed to get credentials: ${err.message}`)); } } else { authLog('Attempting to fetch token information in "PKCE flow"'); _authRequestForToken(selectedSSOProvider, code) .then(res => res.json()) .then(body => { if (body && body.error) { const errorType = (body === null || body === void 0 ? void 0 : body.error) || 'unknown'; const errorDesc = body['error_description'] || 'unknown'; const errorMsg = `Error detected after auth token request, aborting. Error: ${errorType}, Error description: ${errorDesc}`; reject(new Error(errorMsg)); } else { authLog('Successfully acquired token results'); authDebug('PKCE flow result', body); storeRefreshTokenData(body.refresh_token, selectedSSOProvider.id); try { const credentials = getCredentialsFromAuthResult(body, selectedSSOProvider); resolve(credentials); } catch (e) { reject(new Error(`Failed to get credentials: ${e.message}`)); } } }) .catch(err => { reject(new Error(`Acquiring token results for PKCE auth flow failed, err: ${err}`)); }); } }); const _authRequestForToken = (selectedSSOProvider, code) => { const redirectUri = assembleRedirectUri(selectedSSOProvider.id); authLog(`For token request, assembled redirect_uri to be: ${redirectUri}`); const SSOParams = selectedSSOProvider.params; let details = { grant_type: defaultTokenGrantType, client_id: SSOParams.client_id, redirect_uri: redirectUri, code_verifier: window.sessionStorage.getItem(AUTH_STORAGE_CODE_VERIFIER), code }; window.sessionStorage.removeItem(AUTH_STORAGE_CODE_VERIFIER); const SSOExtraTokenParams = selectedSSOProvider.token_params || {}; if (SSOExtraTokenParams) { details = Object.assign(Object.assign({}, details), SSOExtraTokenParams); } const requestBody = assembleRequestBody(details); authLog(`Request for token in PKCE flow, idp_id: ${selectedSSOProvider.id}`); const requestUrl = selectedSSOProvider.token_endpoint; return window.fetch(requestUrl, Object.assign(Object.assign({}, tokenRequestOptions), { body: requestBody })); }; export const handleRefreshingToken = (SSOProviders) => new Promise((resolve, reject) => { authLog('Handling refreshing of tokens'); const { refreshToken, selectedSSOProviderId } = retrieveRefreshTokenData(); if (!refreshToken) { reject(new Error('Could not retrieve a valid refresh token, aborting. Make sure to use the PKCE auth flow.')); return; } const selectedSSOProvider = SSOProviders.find(({ id }) => id === selectedSSOProviderId); if (!selectedSSOProvider) { reject(new Error('Could not find SSO provider data for refreshing token, aborting')); return; } const SSOParams = selectedSSOProvider.params; let details = { grant_type: defaultRefreshGrantType, refresh_token: refreshToken, client_id: SSOParams.client_id, scope: SSOParams.scope }; const SSOExtraTokenParams = selectedSSOProvider.token_params || {}; if (SSOExtraTokenParams) { details = Object.assign(Object.assign({}, details), SSOExtraTokenParams); } const requestBody = assembleRequestBody(details); authLog(`Request to refresh the tokens for idp_id: ${selectedSSOProvider.id}`); const requestUrl = selectedSSOProvider.token_endpoint; window .fetch(requestUrl, Object.assign(Object.assign({}, tokenRequestOptions), { body: requestBody })) .then(res => res.json()) .then(body => { authDebug('Refresh token request result', body); const { refresh_token } = body; if (!refresh_token) { authLog('Missing refresh_token in JSON response after API call', 'warn'); } storeRefreshTokenData(refresh_token, selectedSSOProviderId); const credentials = getCredentialsFromAuthResult(body, selectedSSOProvider); authLog('Successfully retrieved credentials from refreshed token'); resolve(credentials); }) .catch(err => { reject(new Error(`Request to refresh the tokens for idp_id: ${selectedSSOProvider.id} failed, err: ${err}`)); }); });