UNPKG

neo4j-client-sso

Version:

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

242 lines (241 loc) 12.5 kB
"use strict"; /* * 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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.handleRefreshingToken = exports.handleAuthFromRedirect = exports.authRequestForSSO = void 0; const common_1 = require("./common"); const constants_1 = require("./constants"); const helpers_1 = require("./helpers"); const settings_1 = require("./settings"); const tokenRequestOptions = { method: 'post', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } }; const authRequestForSSO = (selectedSSOProvider) => __awaiter(void 0, void 0, void 0, function* () { (0, helpers_1.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.'); } (0, common_1.temporarilyStoreUrlSearchParams)(); const oauth2Endpoint = selectedSSOProvider.auth_endpoint; if (!oauth2Endpoint) { throw new Error(`Invalid OAuth2 endpoint: "${oauth2Endpoint}"`); } (0, helpers_1.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 = (0, helpers_1.assembleRedirectUri)(selectedSSOProvider.id); (0, helpers_1.authLog)(`For auth request, assembled redirect_uri to be: ${redirectUri}`); const SSOParams = Object.assign(Object.assign({}, (selectedSSOProvider.params || {})), { redirect_uri: redirectUri }); const state = (0, helpers_1.createStateForRequest)(); let params = Object.assign(Object.assign({}, SSOParams), { state }); window.sessionStorage.setItem(constants_1.AUTH_STORAGE_STATE, state); const SSOExtraAuthParams = selectedSSOProvider.auth_params || {}; if (SSOExtraAuthParams) { params = Object.assign(Object.assign({}, params), SSOExtraAuthParams); } (0, helpers_1.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: (0, helpers_1.createNonce)() }); (0, helpers_1.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 === constants_1.PKCE) { const codeChallengeMethod = SSOConfig.code_challenge_method || settings_1.defaultCodeChallengeMethod; (0, helpers_1.authLog)(`Auth flow "PKCE", using code_challenge_method: "${codeChallengeMethod}"`); try { const codeVerifier = (0, helpers_1.createCodeVerifier)(codeChallengeMethod) || ''; window.sessionStorage.setItem(constants_1.AUTH_STORAGE_CODE_VERIFIER, codeVerifier); const codeChallenge = yield (0, helpers_1.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 === constants_1.IMPLICIT) { (0, helpers_1.authLog)('Auth flow "implicit flow"'); _submitForm(form, params); } else { throw new Error(`Auth flow "${selectedSSOProvider.auth_flow}" is not supported.`); } }); exports.authRequestForSSO = authRequestForSSO; 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 } = (0, common_1.getInitialisationParameters)(); (0, helpers_1.authLog)('Handling auth redirect from SSO server'); (0, helpers_1.removeSearchParamsInBrowserHistory)(searchParamsToRemove || settings_1.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(constants_1.AUTH_STORAGE_STATE); if (state !== savedState) { reject(new Error('Invalid state parameter, aborting')); return; } window.sessionStorage.removeItem(constants_1.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() === constants_1.BEARER && accessToken) { (0, helpers_1.authLog)('Successfully acquired access_token in "implicit flow"'); (0, helpers_1.authDebug)('Implicit flow id_token', idToken); (0, helpers_1.authDebug)('Implicit flow access_token', accessToken); // INFO: Implicit flow has no support for refresh_token. try { const credentials = (0, common_1.getCredentialsFromAuthResult)({ access_token: accessToken, id_token: idToken }, selectedSSOProvider); resolve(credentials); } catch (err) { reject(new Error(`Failed to get credentials: ${err.message}`)); } } else { (0, helpers_1.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 { (0, helpers_1.authLog)('Successfully acquired token results'); (0, helpers_1.authDebug)('PKCE flow result', body); (0, common_1.storeRefreshTokenData)(body.refresh_token, selectedSSOProvider.id); try { const credentials = (0, common_1.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}`)); }); } }); exports.handleAuthFromRedirect = handleAuthFromRedirect; const _authRequestForToken = (selectedSSOProvider, code) => { const redirectUri = (0, helpers_1.assembleRedirectUri)(selectedSSOProvider.id); (0, helpers_1.authLog)(`For token request, assembled redirect_uri to be: ${redirectUri}`); const SSOParams = selectedSSOProvider.params; let details = { grant_type: settings_1.defaultTokenGrantType, client_id: SSOParams.client_id, redirect_uri: redirectUri, code_verifier: window.sessionStorage.getItem(constants_1.AUTH_STORAGE_CODE_VERIFIER), code }; window.sessionStorage.removeItem(constants_1.AUTH_STORAGE_CODE_VERIFIER); const SSOExtraTokenParams = selectedSSOProvider.token_params || {}; if (SSOExtraTokenParams) { details = Object.assign(Object.assign({}, details), SSOExtraTokenParams); } const requestBody = (0, helpers_1.assembleRequestBody)(details); (0, helpers_1.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 })); }; const handleRefreshingToken = (SSOProviders) => new Promise((resolve, reject) => { (0, helpers_1.authLog)('Handling refreshing of tokens'); const { refreshToken, selectedSSOProviderId } = (0, common_1.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: settings_1.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 = (0, helpers_1.assembleRequestBody)(details); (0, helpers_1.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 => { (0, helpers_1.authDebug)('Refresh token request result', body); const { refresh_token } = body; if (!refresh_token) { (0, helpers_1.authLog)('Missing refresh_token in JSON response after API call', 'warn'); } (0, common_1.storeRefreshTokenData)(refresh_token, selectedSSOProviderId); const credentials = (0, common_1.getCredentialsFromAuthResult)(body, selectedSSOProvider); (0, helpers_1.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}`)); }); }); exports.handleRefreshingToken = handleRefreshingToken;