neo4j-client-sso
Version:
Single sign-on client (frontend) library for Neo4j products
236 lines (235 loc) • 11.9 kB
JavaScript
/*
* 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}`));
});
});