neo4j-client-sso
Version:
Single sign-on client (frontend) library for Neo4j products
242 lines (241 loc) • 12.5 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());
});
};
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;