From 10d8ec835bdf96d13d6933ca26a93c08c1883feb Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Mon, 5 May 2025 13:52:54 -0600 Subject: [PATCH 1/9] modified for idm --- src/api/AuthenticateApi.ts | 37 +- src/api/BaseApi.ts | 5 +- src/ops/AuthenticateOps-backup.ts | 1224 +++++++++++++++++++++++++++++ src/ops/AuthenticateOps.ts | 222 +++++- src/ops/ConfigOps.ts | 541 ++++++------- src/ops/ConnectionProfileOps.ts | 2 +- src/shared/Constants.ts | 6 + 7 files changed, 1731 insertions(+), 306 deletions(-) create mode 100644 src/ops/AuthenticateOps-backup.ts diff --git a/src/api/AuthenticateApi.ts b/src/api/AuthenticateApi.ts index 997a698d6..128c2d0a1 100644 --- a/src/api/AuthenticateApi.ts +++ b/src/api/AuthenticateApi.ts @@ -1,8 +1,8 @@ import util from 'util'; - +import { debugMessage } from '../utils/Console'; import { State } from '../shared/State'; import { getRealmPath } from '../utils/ForgeRockUtils'; -import { generateAmApi } from './BaseApi'; +import { generateAmApi, generateIdmApi } from './BaseApi'; const authenticateUrlTemplate = '%s/json%s/authenticate'; const authenticateWithServiceUrlTemplate = `${authenticateUrlTemplate}?authIndexType=service&authIndexValue=%s`; @@ -73,3 +73,36 @@ export async function step({ }).post(urlString, body, config); return data; } + + +/** + * + * @param {any} body POST request body + * @param {any} config request config + * @param {string} realm realm + * @param {string} service name of authentication service/journey + * @returns Promise resolving to the authentication service response + */ +export async function stepIdm({ + body = {}, + config = {}, + + state, +}: { + body?: object; + config?: object; + realm?: string; + service?: string; + state: State; +}): Promise { + + debugMessage({ + message: `AuthenticateApi.stepIdm: function start `, + state, + }) + const urlString = `${state.getHost()}/authentication?_action=login`; + const response = await generateIdmApi({ + state, + }).post(urlString, body, config); + return response; +} diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index 80365cbd3..87bed8fdb 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -312,6 +312,10 @@ export function generateIdmApi({ ...(state.getBearerToken() && { Authorization: `Bearer ${state.getBearerToken()}`, }), + ...(!state.getBearerToken() && { + 'X-OpenIDM-Username': state.getUsername(), + 'X-OpenIDM-Password': state.getPassword(), + }), }, httpAgent: getHttpAgent(), httpsAgent: getHttpsAgent(state.getAllowInsecureConnection()), @@ -319,7 +323,6 @@ export function generateIdmApi({ }, requestOverride ); - const request = createAxiosInstance(state, requestConfig); // enable curlirizer output in debug mode diff --git a/src/ops/AuthenticateOps-backup.ts b/src/ops/AuthenticateOps-backup.ts new file mode 100644 index 000000000..a95a45a22 --- /dev/null +++ b/src/ops/AuthenticateOps-backup.ts @@ -0,0 +1,1224 @@ +import { createHash, randomBytes } from 'crypto'; +import url from 'url'; +import { v4 } from 'uuid'; + +import { step } from '../api/AuthenticateApi'; +import { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi'; +import Constants from '../shared/Constants'; +import { State } from '../shared/State'; +import { encodeBase64Url } from '../utils/Base64Utils'; +import { debugMessage, verboseMessage } from '../utils/Console'; +import { isValidUrl, parseUrl } from '../utils/ExportImportUtils'; +import { CallbackHandler } from './CallbackOps'; +import { readServiceAccountScopes } from './cloud/EnvServiceAccountScopesOps'; +import { + getServiceAccount, + SERVICE_ACCOUNT_DEFAULT_SCOPES, +} from './cloud/ServiceAccountOps'; +import { + getConnectionProfile, + loadConnectionProfile, + saveConnectionProfile, +} from './ConnectionProfileOps'; +import { FrodoError } from './FrodoError'; +import { createSignedJwtToken, JwkRsa } from './JoseOps'; +import { + accessToken, + type AccessTokenMetaType, + authorize, +} from './OAuth2OidcOps'; +import { getSessionInfo } from './SessionOps'; +import { + hasSaBearerToken, + hasUserBearerToken, + hasUserSessionToken, + readSaBearerToken, + readUserBearerToken, + readUserSessionToken, + saveSaBearerToken, + saveUserBearerToken, + saveUserSessionToken, +} from './TokenCacheOps'; + +export type Authenticate = { + /** + * Get tokens and store them in State + * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) + * @param {boolean} autoRefresh true to automatically refresh tokens before they expire (default: true) + * @param {string[]} types Array of supported deployment types. The function will throw an error if an unsupported type is detected (default: ['classic', 'cloud', 'forgeops']) + * @param {CallbackHandler} callbackHandler function allowing the library to collect responses from the user through callbacks + * @returns {Promise} object containing the tokens + */ + getTokens( + forceLoginAsUser?: boolean, + autoRefresh?: boolean, + types?: string[], + callbackHandler?: CallbackHandler + ): Promise; + + // Deprecated + /** + * Get access token for service account + * @param {string} saId optional service account id + * @param {JwkRsa} saJwk optional service account JWK + * @returns {string | null} Access token or null + * @deprecated since v2.0.0 use {@link Authenticate.getTokens | getTokens} instead + * ```javascript + * getTokens(): Promise + * ``` + * @group Deprecated + */ + getAccessTokenForServiceAccount( + saId?: string, + saJwk?: JwkRsa + ): Promise; +}; + +export default (state: State): Authenticate => { + return { + async getTokens( + forceLoginAsUser = false, + autoRefresh = true, + types = Constants.DEPLOYMENT_TYPES, + callbackHandler = null + ) { + return getTokens({ + forceLoginAsUser, + autoRefresh, + types, + callbackHandler, + state, + }); + }, + + // Deprecated + async getAccessTokenForServiceAccount( + saId: string = undefined, + saJwk: JwkRsa = undefined + ): Promise { + const { access_token } = await getFreshSaBearerToken({ + saId, + saJwk, + state, + }); + return access_token; + }, + }; +}; + +const adminClientPassword = 'doesnotmatter'; +const redirectUrlTemplate = '/platform/appAuthHelperRedirect.html'; + +const s = Constants.AVAILABLE_SCOPES; +const CLOUD_ADMIN_MINIMAL_SCOPES: string[] = [ + s.AnalyticsFullScope, + s.CertificateFullScope, + s.ContentSecurityPolicyFullScope, + s.CookieDomainsFullScope, + s.CustomDomainFullScope, + s.ESVFullScope, + s.AdminFederationFullScope, + s.IdmFullScope, + s.OpenIdScope, + s.PromotionScope, + s.ReleaseFullScope, + s.SSOCookieFullScope, +]; +const CLOUD_ADMIN_DEFAULT_SCOPES: string[] = [ + s.AnalyticsFullScope, + s.AutoAccessFullScope, + s.CertificateFullScope, + s.ContentSecurityPolicyFullScope, + s.CookieDomainsFullScope, + s.CustomDomainFullScope, + s.ESVFullScope, + s.AdminFederationFullScope, + s.IdmFullScope, + s.IGAFullScope, + s.OpenIdScope, + s.PromotionScope, + s.ReleaseFullScope, + s.SSOCookieFullScope, + s.ProxyConnectFullScope, +]; +const FORGEOPS_ADMIN_DEFAULT_SCOPES: string[] = [s.IdmFullScope, s.OpenIdScope]; +const forgeopsAdminScopes = FORGEOPS_ADMIN_DEFAULT_SCOPES.join(' '); +const serviceAccountDefaultScopes = SERVICE_ACCOUNT_DEFAULT_SCOPES.join(' '); + +const fidcClientId = 'idmAdminClient'; +const forgeopsClientId = 'idm-admin-ui'; +let adminClientId = fidcClientId; + +/** + * Helper function to get cookie name + * @param {State} state library state + * @returns {string} cookie name + */ +async function determineCookieName(state: State): Promise { + const data = await getServerInfo({ state }); + debugMessage({ + message: `AuthenticateOps.determineCookieName: cookieName=${data.cookieName}`, + state, + }); + return data.cookieName; +} + +/** + * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey + * @param {Object} payload response from the previous authentication journey step + * @param {State} state library state + * @returns {Object} an object indicating if 2fa is required and the original payload + */ +function checkAndHandle2FA({ + payload, + otpCallbackHandler, + state, +}: { + payload; + otpCallbackHandler: CallbackHandler; + state: State; +}) { + debugMessage({ message: `AuthenticateOps.checkAndHandle2FA: start`, state }); + // let skippable = false; + if ('callbacks' in payload) { + for (let callback of payload.callbacks) { + // select localAuthentication if Admin Federation is enabled + if (callback.type === 'SelectIdPCallback') { + debugMessage({ + message: `AuthenticateOps.checkAndHandle2FA: Admin federation enabled. Allowed providers:`, + state, + }); + let localAuth = false; + for (const value of callback.output[0].value) { + debugMessage({ message: `${value.provider}`, state }); + if (value.provider === 'localAuthentication') { + localAuth = true; + } + } + if (localAuth) { + debugMessage({ message: `local auth allowed`, state }); + callback.input[0].value = 'localAuthentication'; + } else { + debugMessage({ message: `local auth NOT allowed`, state }); + } + } + if (callback.type === 'HiddenValueCallback') { + if (callback.input[0].value.includes('skip')) { + // skippable = true; + callback.input[0].value = 'Skip'; + // debugMessage( + // `AuthenticateOps.checkAndHandle2FA: end [need2fa=true, skippable=true]` + // ); + // return { + // nextStep: true, + // need2fa: true, + // factor: 'None', + // supported: true, + // payload, + // }; + } + if (callback.input[0].value.includes('webAuthnOutcome')) { + // webauthn!!! + debugMessage({ + message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=true, unsupported factor: webauthn]`, + state, + }); + return { + nextStep: false, + need2fa: true, + factor: 'WebAuthN', + supported: false, + payload, + }; + } + } + if (callback.type === 'NameCallback') { + if (callback.output[0].value.includes('code')) { + // skippable = false; + debugMessage({ + message: `AuthenticateOps.checkAndHandle2FA: need2fa=true, skippable=false`, + state, + }); + if (!otpCallbackHandler) + throw new FrodoError( + `2fa required but no otpCallback function provided.` + ); + callback = otpCallbackHandler(callback); + debugMessage({ + message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=true, skippable=false, factor=Code]`, + state, + }); + return { + nextStep: true, + need2fa: true, + factor: 'Code', + supported: true, + payload, + }; + } else { + // answer callback + callback.input[0].value = state.getUsername(); + } + } + if (callback.type === 'PasswordCallback') { + // answer callback + callback.input[0].value = state.getPassword(); + } + } + debugMessage({ + message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=false]`, + state, + }); + // debugMessage(payload); + return { + nextStep: true, + need2fa: false, + factor: 'None', + supported: true, + payload, + }; + } + debugMessage({ + message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=false]`, + state, + }); + // debugMessage(payload); + return { + nextStep: false, + need2fa: false, + factor: 'None', + supported: true, + payload, + }; +} + +/** + * Helper function to set the default realm by deployment type + * @param {State} state library state + */ +function determineDefaultRealm(state: State) { + if (!state.getRealm() || state.getRealm() === Constants.DEFAULT_REALM_KEY) { + state.setRealm( + Constants.DEPLOYMENT_TYPE_REALM_MAP[state.getDeploymentType()] + ); + } +} + +/** + * Helper function to determine the deployment type + * @param {State} state library state + * @returns {Promise} deployment type + */ +async function determineDeploymentType(state: State): Promise { + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: start`, + state, + }); + const cookieValue = state.getCookieValue(); + let deploymentType = state.getDeploymentType(); + + switch (deploymentType) { + case Constants.CLOUD_DEPLOYMENT_TYPE_KEY: + adminClientId = state.getAdminClientId() || fidcClientId; + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, + state, + }); + return deploymentType; + + case Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY: + adminClientId = state.getAdminClientId() || forgeopsClientId; + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, + state, + }); + return deploymentType; + + case Constants.CLASSIC_DEPLOYMENT_TYPE_KEY: + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, + state, + }); + return deploymentType; + + // detect deployment type + default: { + // if we are using a service account, we know it's cloud + if (state.getUseBearerTokenForAmApis()) { + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: end [type=${Constants.CLOUD_DEPLOYMENT_TYPE_KEY}]`, + state, + }); + return Constants.CLOUD_DEPLOYMENT_TYPE_KEY; + } + + const verifier = encodeBase64Url(randomBytes(32)); + const challenge = encodeBase64Url( + createHash('sha256').update(verifier).digest() + ); + const challengeMethod = 'S256'; + const redirectUri = url.resolve(state.getHost(), redirectUrlTemplate); + + const config = { + maxRedirects: 0, + headers: { + [state.getCookieName()]: state.getCookieValue(), + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }; + let bodyFormData = `redirect_uri=${redirectUri}&scope=${s.OpenIdScope}&response_type=code&client_id=${fidcClientId}&csrf=${cookieValue}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`; + + deploymentType = Constants.CLASSIC_DEPLOYMENT_TYPE_KEY; + try { + await authorize({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config, + state, + }); + } catch (e) { + // debugMessage(e.response); + if ( + e.response?.status === 302 && + e.response.headers?.location?.indexOf('code=') > -1 + ) { + verboseMessage({ + message: `ForgeRock Identity Cloud`['brightCyan'] + ` detected.`, + state, + }); + deploymentType = Constants.CLOUD_DEPLOYMENT_TYPE_KEY; + } else { + try { + bodyFormData = `redirect_uri=${redirectUri}&scope=${s.OpenIdScope}&response_type=code&client_id=${forgeopsClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`; + await authorize({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config, + state, + }); + } catch (ex) { + if ( + ex.response?.status === 302 && + ex.response.headers?.location?.indexOf('code=') > -1 + ) { + // maybe we don't want to run through the auto-detect code if we get a custom admin client id? + adminClientId = state.getAdminClientId() || forgeopsClientId; + verboseMessage({ + message: `ForgeOps deployment`['brightCyan'] + ` detected.`, + state, + }); + deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; + } else { + verboseMessage({ + message: `Classic deployment`['brightCyan'] + ` detected.`, + state, + }); + } + } + } + } + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, + state, + }); + return deploymentType; + } + } +} + +/** + * Helper function to extract the semantic version string from a version info object + * @param {Object} versionInfo version info object + * @returns {String} semantic version + */ +function getSemanticVersion(versionInfo) { + if ('version' in versionInfo) { + const versionString = versionInfo.version; + const rx = /([\d]\.[\d]\.[\d](\.[\d])*)/g; + const version = versionString.match(rx); + return version[0]; + } + throw new Error('Cannot extract semantic version from version info object.'); +} + +export type UserSessionMetaType = { + tokenId: string; + successUrl: string; + realm: string; + expires: number; + from_cache?: boolean; +}; + +/** + * Helper function to authenticate and obtain and store session cookie + * @param {State} state library state + * @returns {string} Session token or null + */ +async function getFreshUserSessionToken({ + otpCallbackHandler, + state, +}: { + otpCallbackHandler: CallbackHandler; + state: State; +}): Promise { + debugMessage({ + message: `AuthenticateOps.getFreshUserSessionToken: start`, + state, + }); + const config = { + headers: { + 'X-OpenAM-Username': state.getUsername(), + 'X-OpenAM-Password': state.getPassword(), + }, + }; + let response = await step({ body: {}, config, state }); + + let skip2FA = null; + let steps = 0; + const maxSteps = 3; + do { + skip2FA = checkAndHandle2FA({ + payload: response, + otpCallbackHandler: otpCallbackHandler, + state, + }); + + // throw exception if 2fa required but factor not supported by frodo (e.g. WebAuthN) + if (!skip2FA.supported) { + throw new Error(`Unsupported 2FA factor: ${skip2FA.factor}`); + } + + if (skip2FA.nextStep) { + steps++; + response = await step({ body: skip2FA.payload, state }); + } + + if ('tokenId' in response) { + response['from_cache'] = false; + // get session expiration + const sessionInfo = await getSessionInfo({ + tokenId: response['tokenId'], + state, + }); + response['expires'] = Date.parse(sessionInfo.maxIdleExpirationTime); + debugMessage({ + message: `AuthenticateOps.getFreshUserSessionToken: end [tokenId=${response['tokenId']}]`, + state, + }); + debugMessage({ + message: response, + state, + }); + return response as UserSessionMetaType; + } + } while (skip2FA.nextStep && steps < maxSteps); + debugMessage({ + message: `AuthenticateOps.getFreshUserSessionToken: end [no session]`, + state, + }); + return null; +} + +/** + * Helper function to obtain user session token + * @param {State} state library state + * @returns {Promise} session token or null + */ +async function getUserSessionToken( + otpCallback: CallbackHandler, + state: State +): Promise { + debugMessage({ + message: `AuthenticateOps.getUserSessionToken: start`, + state, + }); + let token: UserSessionMetaType = null; + if (state.getUseTokenCache() && (await hasUserSessionToken({ state }))) { + try { + token = await readUserSessionToken({ state }); + token.from_cache = true; + debugMessage({ + message: `AuthenticateOps.getUserSessionToken: cached`, + state, + }); + } catch (error) { + debugMessage({ + message: `AuthenticateOps.getUserSessionToken: failed cache read`, + state, + }); + } + } + if (!token) { + token = await getFreshUserSessionToken({ + otpCallbackHandler: otpCallback, + state, + }); + token.from_cache = false; + debugMessage({ + message: `AuthenticateOps.getUserSessionToken: fresh`, + state, + }); + } + if (state.getUseTokenCache()) { + await saveUserSessionToken({ token, state }); + } + debugMessage({ + message: `AuthenticateOps.getUserSessionToken: end`, + state, + }); + return token; +} + +async function getAdminUserScopes({ state }: { state: State }) { + debugMessage({ + message: `AuthenticateOps.getAdminUserScopes: start`, + state, + }); + if (state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) { + debugMessage({ + message: `AuthenticateOps.getAdminUserScopes: end with forgeops scopes ${forgeopsAdminScopes}`, + state, + }); + return forgeopsAdminScopes; + } else if ( + state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY + ) { + try { + const availableScopes = (await readServiceAccountScopes({ + flatten: true, + state, + })) as string[]; + availableScopes.push(s.OpenIdScope); + const cloudAdminScopes = CLOUD_ADMIN_DEFAULT_SCOPES.filter((scope) => + availableScopes.includes(scope) + ); + debugMessage({ + message: `AuthenticateOps.getAdminUserScopes: end with cloud scopes ${cloudAdminScopes.join(' ')}`, + state, + }); + return cloudAdminScopes.join(' '); + } catch (error) { + debugMessage({ + message: `AuthenticateOps.getAdminUserScopes: end with minimal cloud scopes ${CLOUD_ADMIN_MINIMAL_SCOPES.join(' ')}`, + state, + }); + return CLOUD_ADMIN_MINIMAL_SCOPES.join(' '); + } + } + debugMessage({ + message: `AuthenticateOps.getAdminUserScopes: end without scopes: Unsupported deployment type: ${state.getDeploymentType()}, expected ${Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY} or ${Constants.CLOUD_DEPLOYMENT_TYPE_KEY}`, + state, + }); + throw new FrodoError( + `Unsupported deployment type: ${state.getDeploymentType()}, expected ${Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY} or ${Constants.CLOUD_DEPLOYMENT_TYPE_KEY}` + ); +} + +/** + * Helper function to obtain an oauth2 authorization code + * @param {string} redirectUri oauth2 redirect uri + * @param {string} codeChallenge PKCE code challenge + * @param {string} codeChallengeMethod PKCE code challenge method + * @param {State} state library state + * @returns {string} oauth2 authorization code or null + */ +async function getAuthCode( + redirectUri: string, + codeChallenge: string, + codeChallengeMethod: string, + state: State +): Promise { + debugMessage({ + message: `AuthenticateOps.getAuthCode: start`, + state, + }); + try { + const bodyFormData = `redirect_uri=${redirectUri}&scope=${await getAdminUserScopes( + { state } + )}&response_type=code&client_id=${adminClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${codeChallenge}&code_challenge_method=${codeChallengeMethod}`; + const config = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + maxRedirects: 0, + }; + let response = undefined; + try { + response = await authorize({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config, + state, + }); + } catch (error) { + response = error.response; + if (response.status < 200 || response.status > 399) { + throw error; + } + } + const redirectLocationURL = response.headers?.location; + const queryObject = url.parse(redirectLocationURL, true).query; + if ('code' in queryObject) { + debugMessage({ + message: `AuthenticateOps.getAuthCode: end with code`, + state, + }); + return queryObject.code as string; + } + debugMessage({ + message: `AuthenticateOps.getAuthCode: end without code`, + state, + }); + throw new FrodoError(`Authz code not found`); + } catch (error) { + debugMessage({ + message: `AuthenticateOps.getAuthCode: end without code`, + state, + }); + throw new FrodoError(`Error getting authz code`, error); + } +} + +/** + * Helper function to obtain oauth2 access token + * @param {State} state library state + * @returns {Promise} access token or null + */ +async function getFreshUserBearerToken({ + state, +}: { + state: State; +}): Promise { + debugMessage({ + message: `AuthenticateOps.getAccessTokenForUser: start`, + state, + }); + try { + const verifier = encodeBase64Url(randomBytes(32)); + const challenge = encodeBase64Url( + createHash('sha256').update(verifier).digest() + ); + const challengeMethod = 'S256'; + const redirectUri = url.resolve( + state.getHost(), + state.getAdminClientRedirectUri() || redirectUrlTemplate + ); + const authCode = await getAuthCode( + redirectUri, + challenge, + challengeMethod, + state + ); + let response: AccessTokenMetaType = null; + if (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY) { + const config = { + auth: { + username: adminClientId, + password: adminClientPassword, + }, + }; + const bodyFormData = `redirect_uri=${redirectUri}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`; + response = await accessToken({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config, + state, + }); + } else { + const bodyFormData = `client_id=${adminClientId}&redirect_uri=${redirectUri}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`; + response = await accessToken({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config: {}, + state, + }); + } + if ('access_token' in response) { + debugMessage({ + message: `AuthenticateOps.getAccessTokenForUser: end with token`, + state, + }); + return response; + } + throw new FrodoError(`No access token in response`); + } catch (error) { + throw new FrodoError(`Error getting access token for user`, error); + } +} + +/** + * Helper function to obtain oauth2 access token + * @param {State} state library state + * @returns {Promise} access token or null + */ +async function getUserBearerToken(state: State): Promise { + debugMessage({ + message: `AuthenticateOps.getUserBearerToken: start`, + state, + }); + let token: AccessTokenMetaType = null; + if (state.getUseTokenCache() && (await hasUserBearerToken({ state }))) { + try { + token = await readUserBearerToken({ state }); + token.from_cache = true; + debugMessage({ + message: `AuthenticateOps.getUserBearerToken: end [cached]`, + state, + }); + } catch (error) { + debugMessage({ + message: `AuthenticateOps.getUserBearerToken: end [failed cache read]`, + state, + }); + } + } + if (!token) { + token = await getFreshUserBearerToken({ state }); + token.from_cache = false; + debugMessage({ + message: `AuthenticateOps.getUserBearerToken: end [fresh]`, + state, + }); + } + if (state.getUseTokenCache()) { + await saveUserBearerToken({ token, state }); + } + return token; +} + +function createPayload(serviceAccountId: string, host: string) { + const u = parseUrl(host); + const aud = `${u.origin}:${ + u.port ? u.port : u.protocol === 'https' ? '443' : '80' + }${u.pathname}/oauth2/access_token`; + + // Cross platform way of setting JWT expiry time 3 minutes in the future, expressed as number of seconds since EPOCH + const exp = Math.floor(new Date().getTime() / 1000 + 180); + + // A unique ID for the JWT which is required when requesting the openid scope + const jti = v4(); + + const iss = serviceAccountId; + const sub = serviceAccountId; + + // Create the payload for our bearer token + const payload = { iss, sub, aud, exp, jti }; + + return payload; +} + +/** + * Get fresh access token for service account + * @param {State} state library state + * @returns {Promise} response object containg token, scope, type, and expiration in seconds + */ +export async function getFreshSaBearerToken({ + saId = undefined, + saJwk = undefined, + state, +}: { + saId?: string; + saJwk?: JwkRsa; + state: State; +}): Promise { + debugMessage({ + message: `AuthenticateOps.getFreshSaBearerToken: start`, + state, + }); + saId = saId ? saId : state.getServiceAccountId(); + saJwk = saJwk ? saJwk : state.getServiceAccountJwk(); + const payload = createPayload(saId, state.getHost()); + const jwt = await createSignedJwtToken(payload, saJwk); + const scope = state.getServiceAccountScope() || serviceAccountDefaultScopes; + const bodyFormData = `assertion=${jwt}&client_id=service-account&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=${scope}`; + let response: AccessTokenMetaType; + try { + response = await accessToken({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config: {}, + state, + }); + } catch (error) { + const err: FrodoError = error as FrodoError; + if ( + err.isHttpError && + err.httpErrorText === 'invalid_scope' && + err.httpDescription?.startsWith('Unsupported scope for service account: ') + ) { + const invalidScopes: string[] = err.httpDescription + .substring(39) + .split(','); + const finalScopes: string[] = scope.split(' ').filter((el) => { + return !invalidScopes.includes(el); + }); + const bodyFormData = `assertion=${jwt}&client_id=service-account&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=${finalScopes.join( + ' ' + )}`; + response = await accessToken({ + amBaseUrl: state.getHost(), + data: bodyFormData, + config: {}, + state, + }); + } + } + if ('access_token' in response) { + debugMessage({ + message: `AuthenticateOps.getFreshSaBearerToken: end`, + state, + }); + return response; + } + debugMessage({ + message: `AuthenticateOps.getFreshSaBearerToken: end [No access token in response]`, + state, + }); + return null; +} + +/** + * Get cached or fresh access token for service account + * @param {State} state library state + * @returns {Promise} response object containg token, scope, type, and expiration in seconds + */ +export async function getSaBearerToken({ + state, +}: { + state: State; +}): Promise { + try { + debugMessage({ + message: `AuthenticateOps.getSaBearerToken: start`, + state, + }); + let token: AccessTokenMetaType = null; + if (state.getUseTokenCache() && (await hasSaBearerToken({ state }))) { + try { + token = await readSaBearerToken({ state }); + token.from_cache = true; + debugMessage({ + message: `AuthenticateOps.getSaBearerToken: end [cached]`, + state, + }); + } catch (error) { + debugMessage({ + message: `AuthenticateOps.getSaBearerToken: end [failed cache read]`, + state, + }); + } + } + if (!token) { + token = await getFreshSaBearerToken({ state }); + token.from_cache = false; + debugMessage({ + message: `AuthenticateOps.getSaBearerToken: end [fresh]`, + state, + }); + } + if (state.getUseTokenCache()) { + await saveSaBearerToken({ token, state }); + } + return token; + } catch (error) { + throw new FrodoError( + `Error getting access token for service account`, + error + ); + } +} + +/** + * Helper function to determine deployment type, default realm, and version and update library state + * @param state library state + */ +async function determineDeploymentTypeAndDefaultRealmAndVersion( + state: State +): Promise { + debugMessage({ + message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: start`, + state, + }); + state.setDeploymentType(await determineDeploymentType(state)); + determineDefaultRealm(state); + debugMessage({ + message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: realm=${state.getRealm()}, type=${state.getDeploymentType()}`, + state, + }); + + const versionInfo = await getServerVersionInfo({ state }); + + // https://github.com/rockcarver/frodo-cli/issues/109 + debugMessage({ message: `Full version: ${versionInfo.fullVersion}`, state }); + + const version = await getSemanticVersion(versionInfo); + state.setAmVersion(version); + debugMessage({ + message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: end`, + state, + }); +} + +/** + * Get logged-in subject + * @param {State} state library state + * @returns {string} a string identifying subject type and id + */ +async function getLoggedInSubject(state: State): Promise { + let subjectString = `user ${state.getUsername()}`; + if (state.getUseBearerTokenForAmApis()) { + try { + const name = ( + await getServiceAccount({ + serviceAccountId: state.getServiceAccountId(), + state, + }) + ).name; + subjectString = `service account ${name} [${state.getServiceAccountId()}]`; + } catch (error) { + subjectString = `service account ${state.getServiceAccountId()}`; + } + } + return subjectString; +} + +/** + * Helper method to set, reset, or cancel timer to auto refresh tokens + * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) + * @param {boolean} autoRefresh true to automatically refresh tokens before they expire (default: true) + * @param {State} state library state + */ +function scheduleAutoRefresh( + forceLoginAsUser: boolean, + autoRefresh: boolean, + state: State +) { + let timer = state.getAutoRefreshTimer(); + // clear existing timer + if (timer) { + debugMessage({ + message: `AuthenticateOps.scheduleAutoRefresh: cancel existing timer`, + state, + }); + clearTimeout(timer); + } + // new timer + if (autoRefresh) { + const expires = + state.getDeploymentType() === Constants.CLASSIC_DEPLOYMENT_TYPE_KEY + ? state.getUserSessionTokenMeta()?.expires + : state.getUseBearerTokenForAmApis() + ? state.getBearerTokenMeta()?.expires + : Math.min( + state.getBearerTokenMeta()?.expires, + state.getUserSessionTokenMeta()?.expires + ); + let timeout = expires - Date.now() - 1000 * 25; + if (timeout < 1000 * 30) { + debugMessage({ + message: `Timeout below threshold of 30 seconds (${Math.ceil( + timeout / 1000 + )}), resetting timeout to 10ms.`, + state, + }); + if (timeout < 10) timeout = 10; + } + debugMessage({ + message: `AuthenticateOps.scheduleAutoRefresh: set new timer [${Math.floor( + timeout / 1000 + )}s (${new Date(timeout).getMinutes()}m ${new Date( + timeout + ).getSeconds()}s)]`, + state, + }); + timer = setTimeout(getTokens, timeout, { + forceLoginAsUser, + autoRefresh, + state, + // Volker's Visual Studio Code doesn't want to have it any other way. + }) as unknown as NodeJS.Timeout; + state.setAutoRefreshTimer(timer); + timer.unref(); + } +} + +export type Tokens = { + bearerToken?: AccessTokenMetaType; + userSessionToken?: UserSessionMetaType; + subject?: string; + host?: string; + realm?: string; +}; + +/** + * Get tokens + * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) + * @param {boolean} autoRefresh true to automatically refresh tokens before they expire (default: true) + * @param {State} state library state + * @returns {Promise} object containing the tokens + */ +export async function getTokens({ + forceLoginAsUser = process.env.FRODO_FORCE_LOGIN_AS_USER ? true : false, + autoRefresh = true, + types = Constants.DEPLOYMENT_TYPES, + callbackHandler = null, + state, +}: { + forceLoginAsUser?: boolean; + autoRefresh?: boolean; + types?: string[]; + callbackHandler?: CallbackHandler; + state: State; +}): Promise { + debugMessage({ + message: `AuthenticateOps.getTokens: start, types: ${types}`, + state, + }); + if (!state.getHost()) { + throw new FrodoError(`No host specified`); + } + let usingConnectionProfile: boolean = false; + try { + // if username/password on cli are empty, try to read from connections.json + if ( + state.getUsername() == null && + state.getPassword() == null && + !state.getServiceAccountId() && + !state.getServiceAccountJwk() + ) { + usingConnectionProfile = await loadConnectionProfile({ state }); + + // fail fast if deployment type not applicable + if ( + state.getDeploymentType() && + !types.includes(state.getDeploymentType()) + ) { + throw new FrodoError( + `Unsupported deployment type '${state.getDeploymentType()}'` + ); + } + } + + // if host is not a valid URL, try to locate a valid URL and deployment type from connections.json + if (!isValidUrl(state.getHost())) { + const conn = await getConnectionProfile({ state }); + state.setHost(conn.tenant); + state.setAllowInsecureConnection(conn.allowInsecureConnection); + state.setDeploymentType(conn.deploymentType); + + // fail fast if deployment type not applicable + if ( + state.getDeploymentType() && + !types.includes(state.getDeploymentType()) + ) { + throw new FrodoError( + `Unsupported deployment type '${state.getDeploymentType()}'` + ); + } + } + + // now that we have the full tenant URL we can lookup the cookie name + state.setCookieName(await determineCookieName(state)); + + // use service account to login? + if ( + !forceLoginAsUser && + (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || + state.getDeploymentType() === undefined) && + state.getServiceAccountId() && + state.getServiceAccountJwk() + ) { + debugMessage({ + message: `AuthenticateOps.getTokens: Authenticating with service account ${state.getServiceAccountId()}`, + state, + }); + try { + const token = await getSaBearerToken({ state }); + if (token) state.setBearerTokenMeta(token); + if (usingConnectionProfile && !token.from_cache) { + saveConnectionProfile({ host: state.getHost(), state }); + } + state.setUseBearerTokenForAmApis(true); + await determineDeploymentTypeAndDefaultRealmAndVersion(state); + + // fail if deployment type not applicable + if ( + state.getDeploymentType() && + !types.includes(state.getDeploymentType()) + ) { + throw new FrodoError( + `Unsupported deployment type: '${state.getDeploymentType()}' not in ${types}` + ); + } + } catch (saErr) { + throw new FrodoError(`Service account login error`, saErr); + } + } + // use user account to login + else if (state.getUsername() && state.getPassword()) { + debugMessage({ + message: `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`, + state, + }); + const token = await getUserSessionToken(callbackHandler, state); + if (token) state.setUserSessionTokenMeta(token); + if (usingConnectionProfile && !token.from_cache) { + saveConnectionProfile({ host: state.getHost(), state }); + } + await determineDeploymentTypeAndDefaultRealmAndVersion(state); + + // fail if deployment type not applicable + if ( + state.getDeploymentType() && + !types.includes(state.getDeploymentType()) + ) { + throw new FrodoError( + `Unsupported deployment type '${state.getDeploymentType()}'` + ); + } + + if ( + state.getCookieValue() && + // !state.getBearerToken() && + (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || + state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) + ) { + const accessToken = await getUserBearerToken(state); + if (accessToken) state.setBearerTokenMeta(accessToken); + } + } + // incomplete or no credentials + else { + throw new FrodoError(`Incomplete or no credentials`); + } + if ( + state.getCookieValue() || + (state.getUseBearerTokenForAmApis() && state.getBearerToken()) + ) { + if (state.getBearerTokenMeta()?.from_cache) { + verboseMessage({ message: `Using cached bearer token.`, state }); + } + if ( + !state.getUseBearerTokenForAmApis() && + state.getUserSessionTokenMeta()?.from_cache + ) { + verboseMessage({ message: `Using cached session token.`, state }); + } + scheduleAutoRefresh(forceLoginAsUser, autoRefresh, state); + const tokens: Tokens = { + bearerToken: state.getBearerTokenMeta(), + userSessionToken: state.getUserSessionTokenMeta(), + subject: await getLoggedInSubject(state), + host: state.getHost(), + realm: state.getRealm() ? state.getRealm() : 'root', + }; + debugMessage({ + message: `AuthenticateOps.getTokens: end with tokens`, + state, + }); + return tokens; + } + } catch (error) { + throw new FrodoError(`Error getting tokens`, error); + } +} \ No newline at end of file diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index e6cbeb861..8cac75b4f 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'crypto'; import url from 'url'; import { v4 } from 'uuid'; -import { step } from '../api/AuthenticateApi'; +import { step, stepIdm } from '../api/AuthenticateApi'; import { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi'; import Constants from '../shared/Constants'; import { State } from '../shared/State'; @@ -15,6 +15,8 @@ import { getServiceAccount, SERVICE_ACCOUNT_DEFAULT_SCOPES, } from './cloud/ServiceAccountOps'; +import axios from 'axios'; +import https from 'https'; import { getConnectionProfile, loadConnectionProfile, @@ -39,6 +41,8 @@ import { saveUserBearerToken, saveUserSessionToken, } from './TokenCacheOps'; +import { getConfigEntities } from '../api/IdmConfigApi'; +import { generateIdmApi } from '../api/BaseApi'; export type Authenticate = { /** @@ -163,6 +167,16 @@ async function determineCookieName(state: State): Promise { return data.cookieName; } +async function determineCookieValueIdm(state: State): Promise { + const idmResponse = await stepIdm({ body: {}, config: {}, state }); + const jwt = idmResponse.headers['set-cookie'][0].split(';')[0].split('=')[1]; + debugMessage({ + message: `AuthenticateOps.determineCookieNameIdm: cookieName = ${jwt}`, + state, + }); + return jwt +} + /** * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey * @param {Object} payload response from the previous authentication journey step @@ -341,6 +355,13 @@ async function determineDeploymentType(state: State): Promise { }); return deploymentType; + case Constants.IDM_DEPLOYMENT_TYPE_KEY: + debugMessage({ + message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, + state, + }); + return deploymentType; + // detect deployment type default: { // if we are using a service account, we know it's cloud @@ -370,6 +391,7 @@ async function determineDeploymentType(state: State): Promise { deploymentType = Constants.CLASSIC_DEPLOYMENT_TYPE_KEY; try { + await authorize({ amBaseUrl: state.getHost(), data: bodyFormData, @@ -378,6 +400,7 @@ async function determineDeploymentType(state: State): Promise { }); } catch (e) { // debugMessage(e.response); + // If error is in that condition after sending Authorize if ( e.response?.status === 302 && e.response.headers?.location?.indexOf('code=') > -1 @@ -409,10 +432,36 @@ async function determineDeploymentType(state: State): Promise { }); deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; } else { - verboseMessage({ - message: `Classic deployment`['brightCyan'] + ` detected.`, - state, - }); + try { + //I need to check if it is idm here + + const idmresponse = await stepIdm({ body: {}, config: {}, state }) + // console.log("status = " + idmresponse.status) + // console.log(" authlogin = " + idmresponse.data.authorization.authLogin) + + verboseMessage({ + message: `idm response = ${JSON.stringify(idmresponse.status, null, 2)} + ${idmresponse.data.authorization.authLogin}`, + state + }) + if (idmresponse.status === 200 && idmresponse.data?.authorization.authLogin) { + verboseMessage({ + message: `Ping Identity IDM deployment`['brightCyan'] + ` detected.`, + state, + }); + deploymentType = Constants.IDM_DEPLOYMENT_TYPE_KEY + verboseMessage({ + message: "deployment type in determine =" + deploymentType, + state,}); + } else { + throw new Error('Not IDM'); + } + } + catch { + verboseMessage({ + message: `Classic deployment`['brightCyan'] + ` detected.`, + state, + }); + } } } } @@ -421,11 +470,36 @@ async function determineDeploymentType(state: State): Promise { message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, state, }); + return deploymentType; } } } + + + + + + + + + + + + + + + + + + + + + + + + /** * Helper function to extract the semantic version string from a version info object * @param {Object} versionInfo version info object @@ -449,6 +523,9 @@ export type UserSessionMetaType = { from_cache?: boolean; }; + + + /** * Helper function to authenticate and obtain and store session cookie * @param {State} state library state @@ -790,7 +867,7 @@ function createPayload(serviceAccountId: string, host: string) { const u = parseUrl(host); const aud = `${u.origin}:${ u.port ? u.port : u.protocol === 'https' ? '443' : '80' - }${u.pathname}/oauth2/access_token`; + }${u.pathname}/oauth2/access_token`; // Cross platform way of setting JWT expiry time 3 minutes in the future, expressed as number of seconds since EPOCH const exp = Math.floor(new Date().getTime() / 1000 + 180); @@ -1010,9 +1087,9 @@ function scheduleAutoRefresh( : state.getUseBearerTokenForAmApis() ? state.getBearerTokenMeta()?.expires : Math.min( - state.getBearerTokenMeta()?.expires, - state.getUserSessionTokenMeta()?.expires - ); + state.getBearerTokenMeta()?.expires, + state.getUserSessionTokenMeta()?.expires + ); let timeout = expires - Date.now() - 1000 * 25; if (timeout < 1000 * 30) { debugMessage({ @@ -1050,6 +1127,39 @@ export type Tokens = { realm?: string; }; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /** * Get tokens * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) @@ -1098,6 +1208,7 @@ export async function getTokens({ ); } } + //username and password empty if ended // if host is not a valid URL, try to locate a valid URL and deployment type from connections.json if (!isValidUrl(state.getHost())) { @@ -1105,7 +1216,6 @@ export async function getTokens({ state.setHost(conn.tenant); state.setAllowInsecureConnection(conn.allowInsecureConnection); state.setDeploymentType(conn.deploymentType); - // fail fast if deployment type not applicable if ( state.getDeploymentType() && @@ -1116,9 +1226,29 @@ export async function getTokens({ ); } } + if (state.getDeploymentType() === undefined) { + const depType = await determineDeploymentType(state); + if(depType === Constants.IDM_DEPLOYMENT_TYPE_KEY){ + state.setDeploymentType(depType) + } + } + determineDefaultRealm(state); + + // console.log("deployment type = " + state.getDeploymentType()); + // const detype = await determineDeploymentType(state) + // console.log(detype) + // console.log("deploymenttype - == " + state.getDeploymentType()); + + + //check if it is idm deployment type, then it will just do some stuff for idm and break + if (state.getDeploymentType() !== Constants.IDM_DEPLOYMENT_TYPE_KEY) { + // now that we have the full tenant URL we can lookup the cookie name + state.setCookieName(await determineCookieName(state)); + } + + + - // now that we have the full tenant URL we can lookup the cookie name - state.setCookieName(await determineCookieName(state)); // use service account to login? if ( @@ -1154,43 +1284,67 @@ export async function getTokens({ throw new FrodoError(`Service account login error`, saErr); } } + + + + + // use user account to login else if (state.getUsername() && state.getPassword()) { debugMessage({ message: `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`, state, }); - const token = await getUserSessionToken(callbackHandler, state); - if (token) state.setUserSessionTokenMeta(token); - if (usingConnectionProfile && !token.from_cache) { - saveConnectionProfile({ host: state.getHost(), state }); - } - await determineDeploymentTypeAndDefaultRealmAndVersion(state); - // fail if deployment type not applicable - if ( - state.getDeploymentType() && - !types.includes(state.getDeploymentType()) - ) { - throw new FrodoError( - `Unsupported deployment type '${state.getDeploymentType()}'` - ); + // if logging into on prem idm + if (state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY) { + const token: Tokens = { + subject: state.getUsername(), + host: state.getHost(), + realm: state.getRealm() ? state.getRealm() : 'root', + }; + //console.log(" token realm is = " + token.realm) + saveConnectionProfile({ host: state.getHost(), state }) + // console.log("successfully saved the new connection profile with " + state.getHost()); + return token + + } + else { + const token = await getUserSessionToken(callbackHandler, state); + if (token) state.setUserSessionTokenMeta(token); + if (usingConnectionProfile && !token.from_cache) { + saveConnectionProfile({ host: state.getHost(), state }); + } + await determineDeploymentTypeAndDefaultRealmAndVersion(state); - if ( - state.getCookieValue() && - // !state.getBearerToken() && - (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || - state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) - ) { - const accessToken = await getUserBearerToken(state); - if (accessToken) state.setBearerTokenMeta(accessToken); + // fail if deployment type not applicable + if ( + state.getDeploymentType() && + !types.includes(state.getDeploymentType()) + ) { + throw new FrodoError( + `Unsupported deployment type '${state.getDeploymentType()}'` + ); + } + + if ( + state.getCookieValue() && + // !state.getBearerToken() && + (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || + state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) + ) { + const accessToken = await getUserBearerToken(state); + if (accessToken) state.setBearerTokenMeta(accessToken); + } } } // incomplete or no credentials else { throw new FrodoError(`Incomplete or no credentials`); } + + if ( state.getCookieValue() || (state.getUseBearerTokenForAmApis() && state.getBearerToken()) diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index e82e26fc5..0b3db297e 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -349,6 +349,8 @@ export async function exportFullConfiguration({ const isForgeOpsDeployment = state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; + const isIdmDeployment = state.getDeploymentType() ===Constants.IDM_DEPLOYMENT_TYPE_KEY; + const isexceptIdm = isPlatformDeployment || isClassicDeployment; const config = await exportAmConfigEntities({ includeReadOnly, @@ -358,7 +360,7 @@ export async function exportFullConfiguration({ }); let globalConfig = {} as FullGlobalExportInterface; - if (!onlyRealm || onlyGlobal) { + if (!onlyRealm || onlyGlobal) { // Export mappings const mappings = await exportWithErrorHandling( exportMappings, @@ -372,7 +374,7 @@ export async function exportFullConfiguration({ state, }, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ); // Export servers and server properties @@ -409,7 +411,7 @@ export async function exportFullConfiguration({ exportEmailTemplates, stateObj, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) )?.emailTemplate, idm: ( @@ -423,7 +425,7 @@ export async function exportFullConfiguration({ state, }, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) )?.idm, internalRole: ( @@ -431,7 +433,7 @@ export async function exportFullConfiguration({ exportInternalRoles, stateObj, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) )?.internalRole, mapping: mappings?.mapping, @@ -499,7 +501,7 @@ export async function exportFullConfiguration({ Object.keys(globalConfig.idm) .filter( (k) => - k === 'ui/themerealm' || + (k === 'ui/themerealm' && !isIdmDeployment) || k === 'sync' || k.startsWith('mapping/') || k.startsWith('emailTemplate/') @@ -509,7 +511,7 @@ export async function exportFullConfiguration({ } const realmConfig = {}; - if (!onlyGlobal || onlyRealm) { + if (!isIdmDeployment && (!onlyGlobal || onlyRealm)) { // Export realm configs const activeRealm = state.getRealm(); for (const realm of Object.keys(config.realm)) { @@ -724,6 +726,7 @@ export async function importFullConfiguration({ const isForgeOpsDeployment = state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; + const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; const { reUuidJourneys, reUuidScripts, @@ -860,7 +863,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'IDM Config Entities', - isPlatformDeployment && !!importData.global.idm + (isPlatformDeployment || isIdmDeployment) && !!importData.global.idm ) ); response.push( @@ -873,7 +876,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'Email Templates', - isPlatformDeployment && !!importData.global.emailTemplate + (isPlatformDeployment || isIdmDeployment) && !!importData.global.emailTemplate ) ); response.push( @@ -887,7 +890,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'Mappings', - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) ); response.push( @@ -938,7 +941,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'Internal Roles', - isPlatformDeployment && !!importData.global.internalRole + (isPlatformDeployment || isIdmDeployment) && !!importData.global.internalRole ) ); stopProgressIndicator({ @@ -947,281 +950,283 @@ export async function importFullConfiguration({ status: 'success', state, }); - // Import to realms - const currentRealm = state.getRealm(); - for (const realm of Object.keys(importData.realm)) { - state.setRealm(getRealmUsingExportFormat(realm)); + if (!isIdmDeployment) { + // Import to realms + const currentRealm = state.getRealm(); + for (const realm of Object.keys(importData.realm)) { + state.setRealm(getRealmUsingExportFormat(realm)); + indicatorId = createProgressIndicator({ + total: 17, + message: `Importing everything for ${realm} realm...`, + state, + }); + // Order of imports matter here since we want dependencies to be imported first. For example, journeys depend on a lot of things, so they are last, and many things depend on scripts, so they are first. + response.push( + await importWithErrorHandling( + importScripts, + { + scriptName: '', + importData: importData.realm[realm], + options: { + deps: false, + reUuid: reUuidScripts, + includeDefault, + }, + validate: false, + state, + }, + errors, + indicatorId, + 'Scripts', + !!importData.realm[realm].script + ) + ); + response.push( + await importWithErrorHandling( + importThemes, + { + importData: importData.realm[realm], + state, + }, + errors, + indicatorId, + 'Themes', + isPlatformDeployment && !!importData.realm[realm].theme + ) + ); + response.push( + await importWithErrorHandling( + importSecretStores, + { + importData: importData.realm[realm], + globalConfig: false, + secretStoreId: '', + state, + }, + errors, + indicatorId, + 'Secret Stores', + isClassicDeployment && !!importData.realm[realm].secretstore + ) + ); + response.push( + await importWithErrorHandling( + importAgentGroups, + { importData: importData.realm[realm], state }, + errors, + indicatorId, + 'Agent Groups', + !!importData.realm[realm].agentGroup + ) + ); + response.push( + await importWithErrorHandling( + importAgents, + { importData: importData.realm[realm], globalConfig: false, state }, + errors, + indicatorId, + 'Agents', + !!importData.realm[realm].agent + ) + ); + response.push( + await importWithErrorHandling( + importResourceTypes, + { + importData: importData.realm[realm], + state, + }, + errors, + indicatorId, + 'Resource Types', + !!importData.realm[realm].resourcetype + ) + ); + response.push( + await importWithErrorHandling( + importCirclesOfTrust, + { + importData: importData.realm[realm], + state, + }, + errors, + indicatorId, + 'Circles of Trust', + !!importData.realm[realm].saml && !!importData.realm[realm].saml.cot + ) + ); + response.push( + await importWithErrorHandling( + importSaml2Providers, + { + importData: importData.realm[realm], + options: { deps: false }, + state, + }, + errors, + indicatorId, + 'Saml2 Providers', + !!importData.realm[realm].saml + ) + ); + response.push( + await importWithErrorHandling( + importSocialIdentityProviders, + { + importData: importData.realm[realm], + options: { deps: false }, + state, + }, + errors, + indicatorId, + 'Social Identity Providers', + !!importData.realm[realm].idp + ) + ); + response.push( + await importWithErrorHandling( + importOAuth2Clients, + { + importData: importData.realm[realm], + options: { deps: false }, + state, + }, + errors, + indicatorId, + 'OAuth2 Clients', + !!importData.realm[realm].application + ) + ); + response.push( + await importWithErrorHandling( + importOAuth2TrustedJwtIssuers, + { + importData: importData.realm[realm], + state, + }, + errors, + indicatorId, + 'Trusted JWT Issuers', + !!importData.realm[realm].trustedJwtIssuer + ) + ); + response.push( + await importWithErrorHandling( + importApplications, + { + importData: importData.realm[realm], + options: { deps: false }, + state, + }, + errors, + indicatorId, + 'Applications', + isPlatformDeployment && !!importData.realm[realm].managedApplication + ) + ); + response.push( + await importWithErrorHandling( + importPolicySets, + { + importData: importData.realm[realm], + options: { deps: false, prereqs: false }, + state, + }, + errors, + indicatorId, + 'Policy Sets', + !!importData.realm[realm].policyset + ) + ); + response.push( + await importWithErrorHandling( + importPolicies, + { + importData: importData.realm[realm], + options: { deps: false, prereqs: false }, + state, + }, + errors, + indicatorId, + 'Policies', + !!importData.realm[realm].policy + ) + ); + response.push( + await importWithErrorHandling( + importJourneys, + { + importData: importData.realm[realm], + options: { deps: false, reUuid: reUuidJourneys }, + state, + }, + errors, + indicatorId, + 'Journeys', + !!importData.realm[realm].trees + ) + ); + response.push( + await importWithErrorHandling( + importServices, + { + importData: importData.realm[realm], + options: { clean: cleanServices, global: false, realm: true }, + state, + }, + errors, + indicatorId, + 'Services', + !!importData.realm[realm].service + ) + ); + response.push( + await importWithErrorHandling( + importAuthenticationSettings, + { + importData: importData.realm[realm], + globalConfig: false, + state, + }, + errors, + indicatorId, + 'Authentication Settings', + !!importData.realm[realm].authentication + ) + ); + stopProgressIndicator({ + id: indicatorId, + message: `Finished Importing Everything to ${realm} realm!`, + status: 'success', + state, + }); + } + state.setRealm(currentRealm); + // Import everything else indicatorId = createProgressIndicator({ - total: 17, - message: `Importing everything for ${realm} realm...`, + total: 1, + message: `Importing all other AM config entities`, state, }); - // Order of imports matter here since we want dependencies to be imported first. For example, journeys depend on a lot of things, so they are last, and many things depend on scripts, so they are first. - response.push( - await importWithErrorHandling( - importScripts, - { - scriptName: '', - importData: importData.realm[realm], - options: { - deps: false, - reUuid: reUuidScripts, - includeDefault, - }, - validate: false, - state, - }, - errors, - indicatorId, - 'Scripts', - !!importData.realm[realm].script - ) - ); response.push( await importWithErrorHandling( - importThemes, + importAmConfigEntities, { - importData: importData.realm[realm], + importData: importData as unknown as ConfigEntityExportInterface, state, }, errors, indicatorId, - 'Themes', - isPlatformDeployment && !!importData.realm[realm].theme - ) - ); - response.push( - await importWithErrorHandling( - importSecretStores, - { - importData: importData.realm[realm], - globalConfig: false, - secretStoreId: '', - state, - }, - errors, - indicatorId, - 'Secret Stores', - isClassicDeployment && !!importData.realm[realm].secretstore - ) - ); - response.push( - await importWithErrorHandling( - importAgentGroups, - { importData: importData.realm[realm], state }, - errors, - indicatorId, - 'Agent Groups', - !!importData.realm[realm].agentGroup - ) - ); - response.push( - await importWithErrorHandling( - importAgents, - { importData: importData.realm[realm], globalConfig: false, state }, - errors, - indicatorId, - 'Agents', - !!importData.realm[realm].agent - ) - ); - response.push( - await importWithErrorHandling( - importResourceTypes, - { - importData: importData.realm[realm], - state, - }, - errors, - indicatorId, - 'Resource Types', - !!importData.realm[realm].resourcetype - ) - ); - response.push( - await importWithErrorHandling( - importCirclesOfTrust, - { - importData: importData.realm[realm], - state, - }, - errors, - indicatorId, - 'Circles of Trust', - !!importData.realm[realm].saml && !!importData.realm[realm].saml.cot - ) - ); - response.push( - await importWithErrorHandling( - importSaml2Providers, - { - importData: importData.realm[realm], - options: { deps: false }, - state, - }, - errors, - indicatorId, - 'Saml2 Providers', - !!importData.realm[realm].saml - ) - ); - response.push( - await importWithErrorHandling( - importSocialIdentityProviders, - { - importData: importData.realm[realm], - options: { deps: false }, - state, - }, - errors, - indicatorId, - 'Social Identity Providers', - !!importData.realm[realm].idp - ) - ); - response.push( - await importWithErrorHandling( - importOAuth2Clients, - { - importData: importData.realm[realm], - options: { deps: false }, - state, - }, - errors, - indicatorId, - 'OAuth2 Clients', - !!importData.realm[realm].application - ) - ); - response.push( - await importWithErrorHandling( - importOAuth2TrustedJwtIssuers, - { - importData: importData.realm[realm], - state, - }, - errors, - indicatorId, - 'Trusted JWT Issuers', - !!importData.realm[realm].trustedJwtIssuer - ) - ); - response.push( - await importWithErrorHandling( - importApplications, - { - importData: importData.realm[realm], - options: { deps: false }, - state, - }, - errors, - indicatorId, - 'Applications', - isPlatformDeployment && !!importData.realm[realm].managedApplication - ) - ); - response.push( - await importWithErrorHandling( - importPolicySets, - { - importData: importData.realm[realm], - options: { deps: false, prereqs: false }, - state, - }, - errors, - indicatorId, - 'Policy Sets', - !!importData.realm[realm].policyset - ) - ); - response.push( - await importWithErrorHandling( - importPolicies, - { - importData: importData.realm[realm], - options: { deps: false, prereqs: false }, - state, - }, - errors, - indicatorId, - 'Policies', - !!importData.realm[realm].policy - ) - ); - response.push( - await importWithErrorHandling( - importJourneys, - { - importData: importData.realm[realm], - options: { deps: false, reUuid: reUuidJourneys }, - state, - }, - errors, - indicatorId, - 'Journeys', - !!importData.realm[realm].trees - ) - ); - response.push( - await importWithErrorHandling( - importServices, - { - importData: importData.realm[realm], - options: { clean: cleanServices, global: false, realm: true }, - state, - }, - errors, - indicatorId, - 'Services', - !!importData.realm[realm].service - ) - ); - response.push( - await importWithErrorHandling( - importAuthenticationSettings, - { - importData: importData.realm[realm], - globalConfig: false, - state, - }, - errors, - indicatorId, - 'Authentication Settings', - !!importData.realm[realm].authentication + 'Other AM Config Entities' ) ); stopProgressIndicator({ id: indicatorId, - message: `Finished Importing Everything to ${realm} realm!`, + message: `Finished Importing all other AM config entities!`, status: 'success', state, }); } - state.setRealm(currentRealm); - // Import everything else - indicatorId = createProgressIndicator({ - total: 1, - message: `Importing all other AM config entities`, - state, - }); - response.push( - await importWithErrorHandling( - importAmConfigEntities, - { - importData: importData as unknown as ConfigEntityExportInterface, - state, - }, - errors, - indicatorId, - 'Other AM Config Entities' - ) - ); - stopProgressIndicator({ - id: indicatorId, - message: `Finished Importing all other AM config entities!`, - status: 'success', - state, - }); if (throwErrors && errors.length > 0) { throw new FrodoError(`Error importing full config`, errors); } diff --git a/src/ops/ConnectionProfileOps.ts b/src/ops/ConnectionProfileOps.ts index 7196a2149..ca702dc65 100644 --- a/src/ops/ConnectionProfileOps.ts +++ b/src/ops/ConnectionProfileOps.ts @@ -566,7 +566,7 @@ export async function saveConnectionProfile({ profile.encodedPassword = await dataProtection.encrypt( state.getPassword() ); - + // log API if (state.getLogApiKey()) profile.logApiKey = state.getLogApiKey(); if (state.getLogApiSecret()) diff --git a/src/shared/Constants.ts b/src/shared/Constants.ts index 441406fdf..50668e649 100644 --- a/src/shared/Constants.ts +++ b/src/shared/Constants.ts @@ -3,11 +3,13 @@ export type Constants = { CLASSIC_DEPLOYMENT_TYPE_KEY: string; CLOUD_DEPLOYMENT_TYPE_KEY: string; FORGEOPS_DEPLOYMENT_TYPE_KEY: string; + IDM_DEPLOYMENT_TYPE_KEY : string; DEPLOYMENT_TYPES: string[]; DEPLOYMENT_TYPE_REALM_MAP: { classic: string; cloud: string; forgeops: string; + idm: string; }; FRODO_METADATA_ID: string; FRODO_CONNECTION_PROFILES_PATH_KEY: string; @@ -18,15 +20,18 @@ const DEFAULT_REALM_KEY = '__default__realm__'; const CLASSIC_DEPLOYMENT_TYPE_KEY = 'classic'; const CLOUD_DEPLOYMENT_TYPE_KEY = 'cloud'; const FORGEOPS_DEPLOYMENT_TYPE_KEY = 'forgeops'; +const IDM_DEPLOYMENT_TYPE_KEY = 'idm' const DEPLOYMENT_TYPES = [ CLASSIC_DEPLOYMENT_TYPE_KEY, CLOUD_DEPLOYMENT_TYPE_KEY, FORGEOPS_DEPLOYMENT_TYPE_KEY, + IDM_DEPLOYMENT_TYPE_KEY, ]; const DEPLOYMENT_TYPE_REALM_MAP = { [CLASSIC_DEPLOYMENT_TYPE_KEY]: '/', [CLOUD_DEPLOYMENT_TYPE_KEY]: 'alpha', [FORGEOPS_DEPLOYMENT_TYPE_KEY]: '/', + [IDM_DEPLOYMENT_TYPE_KEY]: '/', }; const FRODO_METADATA_ID = 'frodo'; const FRODO_CONNECTION_PROFILES_PATH_KEY = 'FRODO_CONNECTION_PROFILES_PATH'; @@ -122,6 +127,7 @@ export default { CLASSIC_DEPLOYMENT_TYPE_KEY, CLOUD_DEPLOYMENT_TYPE_KEY, FORGEOPS_DEPLOYMENT_TYPE_KEY, + IDM_DEPLOYMENT_TYPE_KEY, DEPLOYMENT_TYPES, DEPLOYMENT_TYPE_REALM_MAP, FRODO_METADATA_ID, From 842addf7c42176a7e1510271db249872e93d71e3 Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Wed, 7 May 2025 11:49:28 -0600 Subject: [PATCH 2/9] Changes for on prem IDM --- src/ops/AuthenticateOps.ts | 98 ++---------------------------- src/ops/ConfigOps.ts | 34 ++++++----- src/utils/SetupPollyForFrodoLib.ts | 1 + 3 files changed, 25 insertions(+), 108 deletions(-) diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index 8cac75b4f..cd0ec741f 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -434,11 +434,7 @@ async function determineDeploymentType(state: State): Promise { } else { try { //I need to check if it is idm here - const idmresponse = await stepIdm({ body: {}, config: {}, state }) - // console.log("status = " + idmresponse.status) - // console.log(" authlogin = " + idmresponse.data.authorization.authLogin) - verboseMessage({ message: `idm response = ${JSON.stringify(idmresponse.status, null, 2)} + ${idmresponse.data.authorization.authLogin}`, state @@ -476,30 +472,6 @@ async function determineDeploymentType(state: State): Promise { } } - - - - - - - - - - - - - - - - - - - - - - - - /** * Helper function to extract the semantic version string from a version info object * @param {Object} versionInfo version info object @@ -1126,40 +1098,6 @@ export type Tokens = { host?: string; realm?: string; }; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Get tokens * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) @@ -1226,30 +1164,14 @@ export async function getTokens({ ); } } - if (state.getDeploymentType() === undefined) { - const depType = await determineDeploymentType(state); - if(depType === Constants.IDM_DEPLOYMENT_TYPE_KEY){ - state.setDeploymentType(depType) - } + if(state.getHost().endsWith('openidm')){ + state.setDeploymentType(await determineDeploymentType(state)) } - determineDefaultRealm(state); - - // console.log("deployment type = " + state.getDeploymentType()); - // const detype = await determineDeploymentType(state) - // console.log(detype) - // console.log("deploymenttype - == " + state.getDeploymentType()); - - //check if it is idm deployment type, then it will just do some stuff for idm and break - if (state.getDeploymentType() !== Constants.IDM_DEPLOYMENT_TYPE_KEY) { + else { // now that we have the full tenant URL we can lookup the cookie name state.setCookieName(await determineCookieName(state)); } - - - - - // use service account to login? if ( !forceLoginAsUser && @@ -1284,18 +1206,12 @@ export async function getTokens({ throw new FrodoError(`Service account login error`, saErr); } } - - - - - // use user account to login else if (state.getUsername() && state.getPassword()) { debugMessage({ message: `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`, state, }); - // if logging into on prem idm if (state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY) { const token: Tokens = { @@ -1303,12 +1219,8 @@ export async function getTokens({ host: state.getHost(), realm: state.getRealm() ? state.getRealm() : 'root', }; - //console.log(" token realm is = " + token.realm) saveConnectionProfile({ host: state.getHost(), state }) - // console.log("successfully saved the new connection profile with " + state.getHost()); - return token - - + return token } else { const token = await getUserSessionToken(callbackHandler, state); @@ -1343,8 +1255,6 @@ export async function getTokens({ else { throw new FrodoError(`Incomplete or no credentials`); } - - if ( state.getCookieValue() || (state.getUseBearerTokenForAmApis() && state.getBearerToken()) diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index 0b3db297e..d5bf3ff69 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -349,18 +349,19 @@ export async function exportFullConfiguration({ const isForgeOpsDeployment = state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; - const isIdmDeployment = state.getDeploymentType() ===Constants.IDM_DEPLOYMENT_TYPE_KEY; - const isexceptIdm = isPlatformDeployment || isClassicDeployment; - - const config = await exportAmConfigEntities({ - includeReadOnly, - onlyRealm, - onlyGlobal, - state, - }); + const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; + let config = {} as ConfigEntityExportInterface + if (!isIdmDeployment) { + config = await exportAmConfigEntities({ + includeReadOnly, + onlyRealm, + onlyGlobal, + state, + }); + } let globalConfig = {} as FullGlobalExportInterface; - if (!onlyRealm || onlyGlobal) { + if (!onlyRealm || onlyGlobal) { // Export mappings const mappings = await exportWithErrorHandling( exportMappings, @@ -471,7 +472,12 @@ export async function exportFullConfiguration({ )?.secretstore, server: serverExport, service: ( - await exportWithErrorHandling(exportServices, globalStateObj, errors) + await exportWithErrorHandling( + exportServices, + globalStateObj, + errors, + !isIdmDeployment + ) )?.service, site: ( await exportWithErrorHandling( @@ -518,8 +524,7 @@ export async function exportFullConfiguration({ const currentRealm = getRealmUsingExportFormat(realm); if ( onlyRealm && - (activeRealm.startsWith('/') ? activeRealm : '/' + activeRealm) !== - currentRealm + (activeRealm.startsWith('/') ? activeRealm : '/' + activeRealm) !== currentRealm ) { continue; } @@ -727,6 +732,7 @@ export async function importFullConfiguration({ state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; + const { reUuidJourneys, reUuidScripts, @@ -904,7 +910,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'Services', - !!importData.global.service + !isIdmDeployment && !!importData.global.service ) ); response.push( diff --git a/src/utils/SetupPollyForFrodoLib.ts b/src/utils/SetupPollyForFrodoLib.ts index 6ff0f759c..fa4464530 100644 --- a/src/utils/SetupPollyForFrodoLib.ts +++ b/src/utils/SetupPollyForFrodoLib.ts @@ -26,6 +26,7 @@ const FRODO_MOCK_HOSTS = process.env.FRODO_MOCK_HOSTS 'https://openam-volker-demo.forgeblocks.com', 'https://nightly.gcp.forgeops.com', 'http://openam-frodo-dev.classic.com:8080', + 'http://openidm-frodo-dev.classic.com:9080' ]; let recordIfMissing = false; From c756f58f54de4874754bf57ad6d45503315bc632 Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Mon, 12 May 2025 14:26:31 -0600 Subject: [PATCH 3/9] ran lint --- src/api/AuthenticateApi.ts | 15 +++--- src/ops/AuthenticateOps-backup.ts | 4 +- src/ops/AuthenticateOps.ts | 75 +++++++++++++----------------- src/ops/ConfigOps.ts | 21 +++++---- src/ops/ConnectionProfileOps.ts | 2 +- src/shared/Constants.ts | 4 +- src/utils/SetupPollyForFrodoLib.ts | 2 +- 7 files changed, 59 insertions(+), 64 deletions(-) diff --git a/src/api/AuthenticateApi.ts b/src/api/AuthenticateApi.ts index 128c2d0a1..dca60aded 100644 --- a/src/api/AuthenticateApi.ts +++ b/src/api/AuthenticateApi.ts @@ -1,6 +1,7 @@ import util from 'util'; -import { debugMessage } from '../utils/Console'; + import { State } from '../shared/State'; +import { debugMessage } from '../utils/Console'; import { getRealmPath } from '../utils/ForgeRockUtils'; import { generateAmApi, generateIdmApi } from './BaseApi'; @@ -74,7 +75,6 @@ export async function step({ return data; } - /** * * @param {any} body POST request body @@ -95,13 +95,12 @@ export async function stepIdm({ service?: string; state: State; }): Promise { - - debugMessage({ - message: `AuthenticateApi.stepIdm: function start `, - state, - }) + debugMessage({ + message: `AuthenticateApi.stepIdm: function start `, + state, + }); const urlString = `${state.getHost()}/authentication?_action=login`; - const response = await generateIdmApi({ + const response = await generateIdmApi({ state, }).post(urlString, body, config); return response; diff --git a/src/ops/AuthenticateOps-backup.ts b/src/ops/AuthenticateOps-backup.ts index a95a45a22..e6cbeb861 100644 --- a/src/ops/AuthenticateOps-backup.ts +++ b/src/ops/AuthenticateOps-backup.ts @@ -1119,7 +1119,7 @@ export async function getTokens({ // now that we have the full tenant URL we can lookup the cookie name state.setCookieName(await determineCookieName(state)); - + // use service account to login? if ( !forceLoginAsUser && @@ -1221,4 +1221,4 @@ export async function getTokens({ } catch (error) { throw new FrodoError(`Error getting tokens`, error); } -} \ No newline at end of file +} diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index cd0ec741f..3ba5d8e6f 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -15,8 +15,6 @@ import { getServiceAccount, SERVICE_ACCOUNT_DEFAULT_SCOPES, } from './cloud/ServiceAccountOps'; -import axios from 'axios'; -import https from 'https'; import { getConnectionProfile, loadConnectionProfile, @@ -41,8 +39,6 @@ import { saveUserBearerToken, saveUserSessionToken, } from './TokenCacheOps'; -import { getConfigEntities } from '../api/IdmConfigApi'; -import { generateIdmApi } from '../api/BaseApi'; export type Authenticate = { /** @@ -167,16 +163,6 @@ async function determineCookieName(state: State): Promise { return data.cookieName; } -async function determineCookieValueIdm(state: State): Promise { - const idmResponse = await stepIdm({ body: {}, config: {}, state }); - const jwt = idmResponse.headers['set-cookie'][0].split(';')[0].split('=')[1]; - debugMessage({ - message: `AuthenticateOps.determineCookieNameIdm: cookieName = ${jwt}`, - state, - }); - return jwt -} - /** * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey * @param {Object} payload response from the previous authentication journey step @@ -391,7 +377,6 @@ async function determineDeploymentType(state: State): Promise { deploymentType = Constants.CLASSIC_DEPLOYMENT_TYPE_KEY; try { - await authorize({ amBaseUrl: state.getHost(), data: bodyFormData, @@ -433,26 +418,35 @@ async function determineDeploymentType(state: State): Promise { deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; } else { try { - //I need to check if it is idm here - const idmresponse = await stepIdm({ body: {}, config: {}, state }) + //I need to check if it is idm here + const idmresponse = await stepIdm({ + body: {}, + config: {}, + state, + }); verboseMessage({ message: `idm response = ${JSON.stringify(idmresponse.status, null, 2)} + ${idmresponse.data.authorization.authLogin}`, - state - }) - if (idmresponse.status === 200 && idmresponse.data?.authorization.authLogin) { + state, + }); + if ( + idmresponse.status === 200 && + idmresponse.data?.authorization.authLogin + ) { verboseMessage({ - message: `Ping Identity IDM deployment`['brightCyan'] + ` detected.`, + message: + `Ping Identity IDM deployment`['brightCyan'] + + ` detected.`, state, }); - deploymentType = Constants.IDM_DEPLOYMENT_TYPE_KEY + deploymentType = Constants.IDM_DEPLOYMENT_TYPE_KEY; verboseMessage({ - message: "deployment type in determine =" + deploymentType, - state,}); + message: 'deployment type in determine =' + deploymentType, + state, + }); } else { throw new Error('Not IDM'); } - } - catch { + } catch { verboseMessage({ message: `Classic deployment`['brightCyan'] + ` detected.`, state, @@ -495,9 +489,6 @@ export type UserSessionMetaType = { from_cache?: boolean; }; - - - /** * Helper function to authenticate and obtain and store session cookie * @param {State} state library state @@ -839,7 +830,7 @@ function createPayload(serviceAccountId: string, host: string) { const u = parseUrl(host); const aud = `${u.origin}:${ u.port ? u.port : u.protocol === 'https' ? '443' : '80' - }${u.pathname}/oauth2/access_token`; + }${u.pathname}/oauth2/access_token`; // Cross platform way of setting JWT expiry time 3 minutes in the future, expressed as number of seconds since EPOCH const exp = Math.floor(new Date().getTime() / 1000 + 180); @@ -1059,9 +1050,9 @@ function scheduleAutoRefresh( : state.getUseBearerTokenForAmApis() ? state.getBearerTokenMeta()?.expires : Math.min( - state.getBearerTokenMeta()?.expires, - state.getUserSessionTokenMeta()?.expires - ); + state.getBearerTokenMeta()?.expires, + state.getUserSessionTokenMeta()?.expires + ); let timeout = expires - Date.now() - 1000 * 25; if (timeout < 1000 * 30) { debugMessage({ @@ -1164,10 +1155,10 @@ export async function getTokens({ ); } } - if(state.getHost().endsWith('openidm')){ - state.setDeploymentType(await determineDeploymentType(state)) + if (state.getHost().endsWith('openidm')) { + state.setDeploymentType(await determineDeploymentType(state)); } - //check if it is idm deployment type, then it will just do some stuff for idm and break + //check if it is idm deployment type, then it will just do some stuff for idm and break else { // now that we have the full tenant URL we can lookup the cookie name state.setCookieName(await determineCookieName(state)); @@ -1212,17 +1203,16 @@ export async function getTokens({ message: `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`, state, }); - // if logging into on prem idm + // if logging into on prem idm if (state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY) { const token: Tokens = { subject: state.getUsername(), host: state.getHost(), realm: state.getRealm() ? state.getRealm() : 'root', }; - saveConnectionProfile({ host: state.getHost(), state }) - return token - } - else { + saveConnectionProfile({ host: state.getHost(), state }); + return token; + } else { const token = await getUserSessionToken(callbackHandler, state); if (token) state.setUserSessionTokenMeta(token); if (usingConnectionProfile && !token.from_cache) { @@ -1244,7 +1234,8 @@ export async function getTokens({ state.getCookieValue() && // !state.getBearerToken() && (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || - state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) + state.getDeploymentType() === + Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) ) { const accessToken = await getUserBearerToken(state); if (accessToken) state.setBearerTokenMeta(accessToken); diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index d5bf3ff69..93e89c1ee 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -349,10 +349,11 @@ export async function exportFullConfiguration({ const isForgeOpsDeployment = state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; - const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; - let config = {} as ConfigEntityExportInterface + const isIdmDeployment = + state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; + let config = {} as ConfigEntityExportInterface; if (!isIdmDeployment) { - config = await exportAmConfigEntities({ + config = await exportAmConfigEntities({ includeReadOnly, onlyRealm, onlyGlobal, @@ -474,7 +475,7 @@ export async function exportFullConfiguration({ service: ( await exportWithErrorHandling( exportServices, - globalStateObj, + globalStateObj, errors, !isIdmDeployment ) @@ -524,7 +525,8 @@ export async function exportFullConfiguration({ const currentRealm = getRealmUsingExportFormat(realm); if ( onlyRealm && - (activeRealm.startsWith('/') ? activeRealm : '/' + activeRealm) !== currentRealm + (activeRealm.startsWith('/') ? activeRealm : '/' + activeRealm) !== + currentRealm ) { continue; } @@ -731,7 +733,8 @@ export async function importFullConfiguration({ const isForgeOpsDeployment = state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; - const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; + const isIdmDeployment = + state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; const { reUuidJourneys, @@ -882,7 +885,8 @@ export async function importFullConfiguration({ errors, indicatorId, 'Email Templates', - (isPlatformDeployment || isIdmDeployment) && !!importData.global.emailTemplate + (isPlatformDeployment || isIdmDeployment) && + !!importData.global.emailTemplate ) ); response.push( @@ -947,7 +951,8 @@ export async function importFullConfiguration({ errors, indicatorId, 'Internal Roles', - (isPlatformDeployment || isIdmDeployment) && !!importData.global.internalRole + (isPlatformDeployment || isIdmDeployment) && + !!importData.global.internalRole ) ); stopProgressIndicator({ diff --git a/src/ops/ConnectionProfileOps.ts b/src/ops/ConnectionProfileOps.ts index ca702dc65..7196a2149 100644 --- a/src/ops/ConnectionProfileOps.ts +++ b/src/ops/ConnectionProfileOps.ts @@ -566,7 +566,7 @@ export async function saveConnectionProfile({ profile.encodedPassword = await dataProtection.encrypt( state.getPassword() ); - + // log API if (state.getLogApiKey()) profile.logApiKey = state.getLogApiKey(); if (state.getLogApiSecret()) diff --git a/src/shared/Constants.ts b/src/shared/Constants.ts index 50668e649..0471c5c91 100644 --- a/src/shared/Constants.ts +++ b/src/shared/Constants.ts @@ -3,7 +3,7 @@ export type Constants = { CLASSIC_DEPLOYMENT_TYPE_KEY: string; CLOUD_DEPLOYMENT_TYPE_KEY: string; FORGEOPS_DEPLOYMENT_TYPE_KEY: string; - IDM_DEPLOYMENT_TYPE_KEY : string; + IDM_DEPLOYMENT_TYPE_KEY: string; DEPLOYMENT_TYPES: string[]; DEPLOYMENT_TYPE_REALM_MAP: { classic: string; @@ -20,7 +20,7 @@ const DEFAULT_REALM_KEY = '__default__realm__'; const CLASSIC_DEPLOYMENT_TYPE_KEY = 'classic'; const CLOUD_DEPLOYMENT_TYPE_KEY = 'cloud'; const FORGEOPS_DEPLOYMENT_TYPE_KEY = 'forgeops'; -const IDM_DEPLOYMENT_TYPE_KEY = 'idm' +const IDM_DEPLOYMENT_TYPE_KEY = 'idm'; const DEPLOYMENT_TYPES = [ CLASSIC_DEPLOYMENT_TYPE_KEY, CLOUD_DEPLOYMENT_TYPE_KEY, diff --git a/src/utils/SetupPollyForFrodoLib.ts b/src/utils/SetupPollyForFrodoLib.ts index fa4464530..7692843cc 100644 --- a/src/utils/SetupPollyForFrodoLib.ts +++ b/src/utils/SetupPollyForFrodoLib.ts @@ -26,7 +26,7 @@ const FRODO_MOCK_HOSTS = process.env.FRODO_MOCK_HOSTS 'https://openam-volker-demo.forgeblocks.com', 'https://nightly.gcp.forgeops.com', 'http://openam-frodo-dev.classic.com:8080', - 'http://openidm-frodo-dev.classic.com:9080' + 'http://openidm-frodo-dev.classic.com:9080', ]; let recordIfMissing = false; From 706ffc6bd5ecdec4bad4ec5de18fde9111704fa7 Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Wed, 14 May 2025 16:21:52 -0600 Subject: [PATCH 4/9] Made changes based on the PR feedback --- src/api/AuthenticateApi.ts | 3 - src/api/BaseApi.ts | 1 + src/ops/AuthenticateOps-backup.ts | 1224 ----------------------------- src/ops/AuthenticateOps.ts | 10 +- src/ops/ConfigOps.ts | 13 +- src/shared/Constants.ts | 2 +- 6 files changed, 10 insertions(+), 1243 deletions(-) delete mode 100644 src/ops/AuthenticateOps-backup.ts diff --git a/src/api/AuthenticateApi.ts b/src/api/AuthenticateApi.ts index dca60aded..b3f92535d 100644 --- a/src/api/AuthenticateApi.ts +++ b/src/api/AuthenticateApi.ts @@ -79,14 +79,11 @@ export async function step({ * * @param {any} body POST request body * @param {any} config request config - * @param {string} realm realm - * @param {string} service name of authentication service/journey * @returns Promise resolving to the authentication service response */ export async function stepIdm({ body = {}, config = {}, - state, }: { body?: object; diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index 87bed8fdb..090fddfd1 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -323,6 +323,7 @@ export function generateIdmApi({ }, requestOverride ); + const request = createAxiosInstance(state, requestConfig); // enable curlirizer output in debug mode diff --git a/src/ops/AuthenticateOps-backup.ts b/src/ops/AuthenticateOps-backup.ts deleted file mode 100644 index e6cbeb861..000000000 --- a/src/ops/AuthenticateOps-backup.ts +++ /dev/null @@ -1,1224 +0,0 @@ -import { createHash, randomBytes } from 'crypto'; -import url from 'url'; -import { v4 } from 'uuid'; - -import { step } from '../api/AuthenticateApi'; -import { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi'; -import Constants from '../shared/Constants'; -import { State } from '../shared/State'; -import { encodeBase64Url } from '../utils/Base64Utils'; -import { debugMessage, verboseMessage } from '../utils/Console'; -import { isValidUrl, parseUrl } from '../utils/ExportImportUtils'; -import { CallbackHandler } from './CallbackOps'; -import { readServiceAccountScopes } from './cloud/EnvServiceAccountScopesOps'; -import { - getServiceAccount, - SERVICE_ACCOUNT_DEFAULT_SCOPES, -} from './cloud/ServiceAccountOps'; -import { - getConnectionProfile, - loadConnectionProfile, - saveConnectionProfile, -} from './ConnectionProfileOps'; -import { FrodoError } from './FrodoError'; -import { createSignedJwtToken, JwkRsa } from './JoseOps'; -import { - accessToken, - type AccessTokenMetaType, - authorize, -} from './OAuth2OidcOps'; -import { getSessionInfo } from './SessionOps'; -import { - hasSaBearerToken, - hasUserBearerToken, - hasUserSessionToken, - readSaBearerToken, - readUserBearerToken, - readUserSessionToken, - saveSaBearerToken, - saveUserBearerToken, - saveUserSessionToken, -} from './TokenCacheOps'; - -export type Authenticate = { - /** - * Get tokens and store them in State - * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) - * @param {boolean} autoRefresh true to automatically refresh tokens before they expire (default: true) - * @param {string[]} types Array of supported deployment types. The function will throw an error if an unsupported type is detected (default: ['classic', 'cloud', 'forgeops']) - * @param {CallbackHandler} callbackHandler function allowing the library to collect responses from the user through callbacks - * @returns {Promise} object containing the tokens - */ - getTokens( - forceLoginAsUser?: boolean, - autoRefresh?: boolean, - types?: string[], - callbackHandler?: CallbackHandler - ): Promise; - - // Deprecated - /** - * Get access token for service account - * @param {string} saId optional service account id - * @param {JwkRsa} saJwk optional service account JWK - * @returns {string | null} Access token or null - * @deprecated since v2.0.0 use {@link Authenticate.getTokens | getTokens} instead - * ```javascript - * getTokens(): Promise - * ``` - * @group Deprecated - */ - getAccessTokenForServiceAccount( - saId?: string, - saJwk?: JwkRsa - ): Promise; -}; - -export default (state: State): Authenticate => { - return { - async getTokens( - forceLoginAsUser = false, - autoRefresh = true, - types = Constants.DEPLOYMENT_TYPES, - callbackHandler = null - ) { - return getTokens({ - forceLoginAsUser, - autoRefresh, - types, - callbackHandler, - state, - }); - }, - - // Deprecated - async getAccessTokenForServiceAccount( - saId: string = undefined, - saJwk: JwkRsa = undefined - ): Promise { - const { access_token } = await getFreshSaBearerToken({ - saId, - saJwk, - state, - }); - return access_token; - }, - }; -}; - -const adminClientPassword = 'doesnotmatter'; -const redirectUrlTemplate = '/platform/appAuthHelperRedirect.html'; - -const s = Constants.AVAILABLE_SCOPES; -const CLOUD_ADMIN_MINIMAL_SCOPES: string[] = [ - s.AnalyticsFullScope, - s.CertificateFullScope, - s.ContentSecurityPolicyFullScope, - s.CookieDomainsFullScope, - s.CustomDomainFullScope, - s.ESVFullScope, - s.AdminFederationFullScope, - s.IdmFullScope, - s.OpenIdScope, - s.PromotionScope, - s.ReleaseFullScope, - s.SSOCookieFullScope, -]; -const CLOUD_ADMIN_DEFAULT_SCOPES: string[] = [ - s.AnalyticsFullScope, - s.AutoAccessFullScope, - s.CertificateFullScope, - s.ContentSecurityPolicyFullScope, - s.CookieDomainsFullScope, - s.CustomDomainFullScope, - s.ESVFullScope, - s.AdminFederationFullScope, - s.IdmFullScope, - s.IGAFullScope, - s.OpenIdScope, - s.PromotionScope, - s.ReleaseFullScope, - s.SSOCookieFullScope, - s.ProxyConnectFullScope, -]; -const FORGEOPS_ADMIN_DEFAULT_SCOPES: string[] = [s.IdmFullScope, s.OpenIdScope]; -const forgeopsAdminScopes = FORGEOPS_ADMIN_DEFAULT_SCOPES.join(' '); -const serviceAccountDefaultScopes = SERVICE_ACCOUNT_DEFAULT_SCOPES.join(' '); - -const fidcClientId = 'idmAdminClient'; -const forgeopsClientId = 'idm-admin-ui'; -let adminClientId = fidcClientId; - -/** - * Helper function to get cookie name - * @param {State} state library state - * @returns {string} cookie name - */ -async function determineCookieName(state: State): Promise { - const data = await getServerInfo({ state }); - debugMessage({ - message: `AuthenticateOps.determineCookieName: cookieName=${data.cookieName}`, - state, - }); - return data.cookieName; -} - -/** - * Helper function to determine if this is a setup mfa prompt in the ID Cloud tenant admin login journey - * @param {Object} payload response from the previous authentication journey step - * @param {State} state library state - * @returns {Object} an object indicating if 2fa is required and the original payload - */ -function checkAndHandle2FA({ - payload, - otpCallbackHandler, - state, -}: { - payload; - otpCallbackHandler: CallbackHandler; - state: State; -}) { - debugMessage({ message: `AuthenticateOps.checkAndHandle2FA: start`, state }); - // let skippable = false; - if ('callbacks' in payload) { - for (let callback of payload.callbacks) { - // select localAuthentication if Admin Federation is enabled - if (callback.type === 'SelectIdPCallback') { - debugMessage({ - message: `AuthenticateOps.checkAndHandle2FA: Admin federation enabled. Allowed providers:`, - state, - }); - let localAuth = false; - for (const value of callback.output[0].value) { - debugMessage({ message: `${value.provider}`, state }); - if (value.provider === 'localAuthentication') { - localAuth = true; - } - } - if (localAuth) { - debugMessage({ message: `local auth allowed`, state }); - callback.input[0].value = 'localAuthentication'; - } else { - debugMessage({ message: `local auth NOT allowed`, state }); - } - } - if (callback.type === 'HiddenValueCallback') { - if (callback.input[0].value.includes('skip')) { - // skippable = true; - callback.input[0].value = 'Skip'; - // debugMessage( - // `AuthenticateOps.checkAndHandle2FA: end [need2fa=true, skippable=true]` - // ); - // return { - // nextStep: true, - // need2fa: true, - // factor: 'None', - // supported: true, - // payload, - // }; - } - if (callback.input[0].value.includes('webAuthnOutcome')) { - // webauthn!!! - debugMessage({ - message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=true, unsupported factor: webauthn]`, - state, - }); - return { - nextStep: false, - need2fa: true, - factor: 'WebAuthN', - supported: false, - payload, - }; - } - } - if (callback.type === 'NameCallback') { - if (callback.output[0].value.includes('code')) { - // skippable = false; - debugMessage({ - message: `AuthenticateOps.checkAndHandle2FA: need2fa=true, skippable=false`, - state, - }); - if (!otpCallbackHandler) - throw new FrodoError( - `2fa required but no otpCallback function provided.` - ); - callback = otpCallbackHandler(callback); - debugMessage({ - message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=true, skippable=false, factor=Code]`, - state, - }); - return { - nextStep: true, - need2fa: true, - factor: 'Code', - supported: true, - payload, - }; - } else { - // answer callback - callback.input[0].value = state.getUsername(); - } - } - if (callback.type === 'PasswordCallback') { - // answer callback - callback.input[0].value = state.getPassword(); - } - } - debugMessage({ - message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=false]`, - state, - }); - // debugMessage(payload); - return { - nextStep: true, - need2fa: false, - factor: 'None', - supported: true, - payload, - }; - } - debugMessage({ - message: `AuthenticateOps.checkAndHandle2FA: end [need2fa=false]`, - state, - }); - // debugMessage(payload); - return { - nextStep: false, - need2fa: false, - factor: 'None', - supported: true, - payload, - }; -} - -/** - * Helper function to set the default realm by deployment type - * @param {State} state library state - */ -function determineDefaultRealm(state: State) { - if (!state.getRealm() || state.getRealm() === Constants.DEFAULT_REALM_KEY) { - state.setRealm( - Constants.DEPLOYMENT_TYPE_REALM_MAP[state.getDeploymentType()] - ); - } -} - -/** - * Helper function to determine the deployment type - * @param {State} state library state - * @returns {Promise} deployment type - */ -async function determineDeploymentType(state: State): Promise { - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: start`, - state, - }); - const cookieValue = state.getCookieValue(); - let deploymentType = state.getDeploymentType(); - - switch (deploymentType) { - case Constants.CLOUD_DEPLOYMENT_TYPE_KEY: - adminClientId = state.getAdminClientId() || fidcClientId; - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, - state, - }); - return deploymentType; - - case Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY: - adminClientId = state.getAdminClientId() || forgeopsClientId; - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, - state, - }); - return deploymentType; - - case Constants.CLASSIC_DEPLOYMENT_TYPE_KEY: - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, - state, - }); - return deploymentType; - - // detect deployment type - default: { - // if we are using a service account, we know it's cloud - if (state.getUseBearerTokenForAmApis()) { - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: end [type=${Constants.CLOUD_DEPLOYMENT_TYPE_KEY}]`, - state, - }); - return Constants.CLOUD_DEPLOYMENT_TYPE_KEY; - } - - const verifier = encodeBase64Url(randomBytes(32)); - const challenge = encodeBase64Url( - createHash('sha256').update(verifier).digest() - ); - const challengeMethod = 'S256'; - const redirectUri = url.resolve(state.getHost(), redirectUrlTemplate); - - const config = { - maxRedirects: 0, - headers: { - [state.getCookieName()]: state.getCookieValue(), - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }; - let bodyFormData = `redirect_uri=${redirectUri}&scope=${s.OpenIdScope}&response_type=code&client_id=${fidcClientId}&csrf=${cookieValue}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`; - - deploymentType = Constants.CLASSIC_DEPLOYMENT_TYPE_KEY; - try { - await authorize({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config, - state, - }); - } catch (e) { - // debugMessage(e.response); - if ( - e.response?.status === 302 && - e.response.headers?.location?.indexOf('code=') > -1 - ) { - verboseMessage({ - message: `ForgeRock Identity Cloud`['brightCyan'] + ` detected.`, - state, - }); - deploymentType = Constants.CLOUD_DEPLOYMENT_TYPE_KEY; - } else { - try { - bodyFormData = `redirect_uri=${redirectUri}&scope=${s.OpenIdScope}&response_type=code&client_id=${forgeopsClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${challenge}&code_challenge_method=${challengeMethod}`; - await authorize({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config, - state, - }); - } catch (ex) { - if ( - ex.response?.status === 302 && - ex.response.headers?.location?.indexOf('code=') > -1 - ) { - // maybe we don't want to run through the auto-detect code if we get a custom admin client id? - adminClientId = state.getAdminClientId() || forgeopsClientId; - verboseMessage({ - message: `ForgeOps deployment`['brightCyan'] + ` detected.`, - state, - }); - deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; - } else { - verboseMessage({ - message: `Classic deployment`['brightCyan'] + ` detected.`, - state, - }); - } - } - } - } - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, - state, - }); - return deploymentType; - } - } -} - -/** - * Helper function to extract the semantic version string from a version info object - * @param {Object} versionInfo version info object - * @returns {String} semantic version - */ -function getSemanticVersion(versionInfo) { - if ('version' in versionInfo) { - const versionString = versionInfo.version; - const rx = /([\d]\.[\d]\.[\d](\.[\d])*)/g; - const version = versionString.match(rx); - return version[0]; - } - throw new Error('Cannot extract semantic version from version info object.'); -} - -export type UserSessionMetaType = { - tokenId: string; - successUrl: string; - realm: string; - expires: number; - from_cache?: boolean; -}; - -/** - * Helper function to authenticate and obtain and store session cookie - * @param {State} state library state - * @returns {string} Session token or null - */ -async function getFreshUserSessionToken({ - otpCallbackHandler, - state, -}: { - otpCallbackHandler: CallbackHandler; - state: State; -}): Promise { - debugMessage({ - message: `AuthenticateOps.getFreshUserSessionToken: start`, - state, - }); - const config = { - headers: { - 'X-OpenAM-Username': state.getUsername(), - 'X-OpenAM-Password': state.getPassword(), - }, - }; - let response = await step({ body: {}, config, state }); - - let skip2FA = null; - let steps = 0; - const maxSteps = 3; - do { - skip2FA = checkAndHandle2FA({ - payload: response, - otpCallbackHandler: otpCallbackHandler, - state, - }); - - // throw exception if 2fa required but factor not supported by frodo (e.g. WebAuthN) - if (!skip2FA.supported) { - throw new Error(`Unsupported 2FA factor: ${skip2FA.factor}`); - } - - if (skip2FA.nextStep) { - steps++; - response = await step({ body: skip2FA.payload, state }); - } - - if ('tokenId' in response) { - response['from_cache'] = false; - // get session expiration - const sessionInfo = await getSessionInfo({ - tokenId: response['tokenId'], - state, - }); - response['expires'] = Date.parse(sessionInfo.maxIdleExpirationTime); - debugMessage({ - message: `AuthenticateOps.getFreshUserSessionToken: end [tokenId=${response['tokenId']}]`, - state, - }); - debugMessage({ - message: response, - state, - }); - return response as UserSessionMetaType; - } - } while (skip2FA.nextStep && steps < maxSteps); - debugMessage({ - message: `AuthenticateOps.getFreshUserSessionToken: end [no session]`, - state, - }); - return null; -} - -/** - * Helper function to obtain user session token - * @param {State} state library state - * @returns {Promise} session token or null - */ -async function getUserSessionToken( - otpCallback: CallbackHandler, - state: State -): Promise { - debugMessage({ - message: `AuthenticateOps.getUserSessionToken: start`, - state, - }); - let token: UserSessionMetaType = null; - if (state.getUseTokenCache() && (await hasUserSessionToken({ state }))) { - try { - token = await readUserSessionToken({ state }); - token.from_cache = true; - debugMessage({ - message: `AuthenticateOps.getUserSessionToken: cached`, - state, - }); - } catch (error) { - debugMessage({ - message: `AuthenticateOps.getUserSessionToken: failed cache read`, - state, - }); - } - } - if (!token) { - token = await getFreshUserSessionToken({ - otpCallbackHandler: otpCallback, - state, - }); - token.from_cache = false; - debugMessage({ - message: `AuthenticateOps.getUserSessionToken: fresh`, - state, - }); - } - if (state.getUseTokenCache()) { - await saveUserSessionToken({ token, state }); - } - debugMessage({ - message: `AuthenticateOps.getUserSessionToken: end`, - state, - }); - return token; -} - -async function getAdminUserScopes({ state }: { state: State }) { - debugMessage({ - message: `AuthenticateOps.getAdminUserScopes: start`, - state, - }); - if (state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) { - debugMessage({ - message: `AuthenticateOps.getAdminUserScopes: end with forgeops scopes ${forgeopsAdminScopes}`, - state, - }); - return forgeopsAdminScopes; - } else if ( - state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY - ) { - try { - const availableScopes = (await readServiceAccountScopes({ - flatten: true, - state, - })) as string[]; - availableScopes.push(s.OpenIdScope); - const cloudAdminScopes = CLOUD_ADMIN_DEFAULT_SCOPES.filter((scope) => - availableScopes.includes(scope) - ); - debugMessage({ - message: `AuthenticateOps.getAdminUserScopes: end with cloud scopes ${cloudAdminScopes.join(' ')}`, - state, - }); - return cloudAdminScopes.join(' '); - } catch (error) { - debugMessage({ - message: `AuthenticateOps.getAdminUserScopes: end with minimal cloud scopes ${CLOUD_ADMIN_MINIMAL_SCOPES.join(' ')}`, - state, - }); - return CLOUD_ADMIN_MINIMAL_SCOPES.join(' '); - } - } - debugMessage({ - message: `AuthenticateOps.getAdminUserScopes: end without scopes: Unsupported deployment type: ${state.getDeploymentType()}, expected ${Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY} or ${Constants.CLOUD_DEPLOYMENT_TYPE_KEY}`, - state, - }); - throw new FrodoError( - `Unsupported deployment type: ${state.getDeploymentType()}, expected ${Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY} or ${Constants.CLOUD_DEPLOYMENT_TYPE_KEY}` - ); -} - -/** - * Helper function to obtain an oauth2 authorization code - * @param {string} redirectUri oauth2 redirect uri - * @param {string} codeChallenge PKCE code challenge - * @param {string} codeChallengeMethod PKCE code challenge method - * @param {State} state library state - * @returns {string} oauth2 authorization code or null - */ -async function getAuthCode( - redirectUri: string, - codeChallenge: string, - codeChallengeMethod: string, - state: State -): Promise { - debugMessage({ - message: `AuthenticateOps.getAuthCode: start`, - state, - }); - try { - const bodyFormData = `redirect_uri=${redirectUri}&scope=${await getAdminUserScopes( - { state } - )}&response_type=code&client_id=${adminClientId}&csrf=${state.getCookieValue()}&decision=allow&code_challenge=${codeChallenge}&code_challenge_method=${codeChallengeMethod}`; - const config = { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - maxRedirects: 0, - }; - let response = undefined; - try { - response = await authorize({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config, - state, - }); - } catch (error) { - response = error.response; - if (response.status < 200 || response.status > 399) { - throw error; - } - } - const redirectLocationURL = response.headers?.location; - const queryObject = url.parse(redirectLocationURL, true).query; - if ('code' in queryObject) { - debugMessage({ - message: `AuthenticateOps.getAuthCode: end with code`, - state, - }); - return queryObject.code as string; - } - debugMessage({ - message: `AuthenticateOps.getAuthCode: end without code`, - state, - }); - throw new FrodoError(`Authz code not found`); - } catch (error) { - debugMessage({ - message: `AuthenticateOps.getAuthCode: end without code`, - state, - }); - throw new FrodoError(`Error getting authz code`, error); - } -} - -/** - * Helper function to obtain oauth2 access token - * @param {State} state library state - * @returns {Promise} access token or null - */ -async function getFreshUserBearerToken({ - state, -}: { - state: State; -}): Promise { - debugMessage({ - message: `AuthenticateOps.getAccessTokenForUser: start`, - state, - }); - try { - const verifier = encodeBase64Url(randomBytes(32)); - const challenge = encodeBase64Url( - createHash('sha256').update(verifier).digest() - ); - const challengeMethod = 'S256'; - const redirectUri = url.resolve( - state.getHost(), - state.getAdminClientRedirectUri() || redirectUrlTemplate - ); - const authCode = await getAuthCode( - redirectUri, - challenge, - challengeMethod, - state - ); - let response: AccessTokenMetaType = null; - if (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY) { - const config = { - auth: { - username: adminClientId, - password: adminClientPassword, - }, - }; - const bodyFormData = `redirect_uri=${redirectUri}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`; - response = await accessToken({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config, - state, - }); - } else { - const bodyFormData = `client_id=${adminClientId}&redirect_uri=${redirectUri}&grant_type=authorization_code&code=${authCode}&code_verifier=${verifier}`; - response = await accessToken({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config: {}, - state, - }); - } - if ('access_token' in response) { - debugMessage({ - message: `AuthenticateOps.getAccessTokenForUser: end with token`, - state, - }); - return response; - } - throw new FrodoError(`No access token in response`); - } catch (error) { - throw new FrodoError(`Error getting access token for user`, error); - } -} - -/** - * Helper function to obtain oauth2 access token - * @param {State} state library state - * @returns {Promise} access token or null - */ -async function getUserBearerToken(state: State): Promise { - debugMessage({ - message: `AuthenticateOps.getUserBearerToken: start`, - state, - }); - let token: AccessTokenMetaType = null; - if (state.getUseTokenCache() && (await hasUserBearerToken({ state }))) { - try { - token = await readUserBearerToken({ state }); - token.from_cache = true; - debugMessage({ - message: `AuthenticateOps.getUserBearerToken: end [cached]`, - state, - }); - } catch (error) { - debugMessage({ - message: `AuthenticateOps.getUserBearerToken: end [failed cache read]`, - state, - }); - } - } - if (!token) { - token = await getFreshUserBearerToken({ state }); - token.from_cache = false; - debugMessage({ - message: `AuthenticateOps.getUserBearerToken: end [fresh]`, - state, - }); - } - if (state.getUseTokenCache()) { - await saveUserBearerToken({ token, state }); - } - return token; -} - -function createPayload(serviceAccountId: string, host: string) { - const u = parseUrl(host); - const aud = `${u.origin}:${ - u.port ? u.port : u.protocol === 'https' ? '443' : '80' - }${u.pathname}/oauth2/access_token`; - - // Cross platform way of setting JWT expiry time 3 minutes in the future, expressed as number of seconds since EPOCH - const exp = Math.floor(new Date().getTime() / 1000 + 180); - - // A unique ID for the JWT which is required when requesting the openid scope - const jti = v4(); - - const iss = serviceAccountId; - const sub = serviceAccountId; - - // Create the payload for our bearer token - const payload = { iss, sub, aud, exp, jti }; - - return payload; -} - -/** - * Get fresh access token for service account - * @param {State} state library state - * @returns {Promise} response object containg token, scope, type, and expiration in seconds - */ -export async function getFreshSaBearerToken({ - saId = undefined, - saJwk = undefined, - state, -}: { - saId?: string; - saJwk?: JwkRsa; - state: State; -}): Promise { - debugMessage({ - message: `AuthenticateOps.getFreshSaBearerToken: start`, - state, - }); - saId = saId ? saId : state.getServiceAccountId(); - saJwk = saJwk ? saJwk : state.getServiceAccountJwk(); - const payload = createPayload(saId, state.getHost()); - const jwt = await createSignedJwtToken(payload, saJwk); - const scope = state.getServiceAccountScope() || serviceAccountDefaultScopes; - const bodyFormData = `assertion=${jwt}&client_id=service-account&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=${scope}`; - let response: AccessTokenMetaType; - try { - response = await accessToken({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config: {}, - state, - }); - } catch (error) { - const err: FrodoError = error as FrodoError; - if ( - err.isHttpError && - err.httpErrorText === 'invalid_scope' && - err.httpDescription?.startsWith('Unsupported scope for service account: ') - ) { - const invalidScopes: string[] = err.httpDescription - .substring(39) - .split(','); - const finalScopes: string[] = scope.split(' ').filter((el) => { - return !invalidScopes.includes(el); - }); - const bodyFormData = `assertion=${jwt}&client_id=service-account&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=${finalScopes.join( - ' ' - )}`; - response = await accessToken({ - amBaseUrl: state.getHost(), - data: bodyFormData, - config: {}, - state, - }); - } - } - if ('access_token' in response) { - debugMessage({ - message: `AuthenticateOps.getFreshSaBearerToken: end`, - state, - }); - return response; - } - debugMessage({ - message: `AuthenticateOps.getFreshSaBearerToken: end [No access token in response]`, - state, - }); - return null; -} - -/** - * Get cached or fresh access token for service account - * @param {State} state library state - * @returns {Promise} response object containg token, scope, type, and expiration in seconds - */ -export async function getSaBearerToken({ - state, -}: { - state: State; -}): Promise { - try { - debugMessage({ - message: `AuthenticateOps.getSaBearerToken: start`, - state, - }); - let token: AccessTokenMetaType = null; - if (state.getUseTokenCache() && (await hasSaBearerToken({ state }))) { - try { - token = await readSaBearerToken({ state }); - token.from_cache = true; - debugMessage({ - message: `AuthenticateOps.getSaBearerToken: end [cached]`, - state, - }); - } catch (error) { - debugMessage({ - message: `AuthenticateOps.getSaBearerToken: end [failed cache read]`, - state, - }); - } - } - if (!token) { - token = await getFreshSaBearerToken({ state }); - token.from_cache = false; - debugMessage({ - message: `AuthenticateOps.getSaBearerToken: end [fresh]`, - state, - }); - } - if (state.getUseTokenCache()) { - await saveSaBearerToken({ token, state }); - } - return token; - } catch (error) { - throw new FrodoError( - `Error getting access token for service account`, - error - ); - } -} - -/** - * Helper function to determine deployment type, default realm, and version and update library state - * @param state library state - */ -async function determineDeploymentTypeAndDefaultRealmAndVersion( - state: State -): Promise { - debugMessage({ - message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: start`, - state, - }); - state.setDeploymentType(await determineDeploymentType(state)); - determineDefaultRealm(state); - debugMessage({ - message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: realm=${state.getRealm()}, type=${state.getDeploymentType()}`, - state, - }); - - const versionInfo = await getServerVersionInfo({ state }); - - // https://github.com/rockcarver/frodo-cli/issues/109 - debugMessage({ message: `Full version: ${versionInfo.fullVersion}`, state }); - - const version = await getSemanticVersion(versionInfo); - state.setAmVersion(version); - debugMessage({ - message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: end`, - state, - }); -} - -/** - * Get logged-in subject - * @param {State} state library state - * @returns {string} a string identifying subject type and id - */ -async function getLoggedInSubject(state: State): Promise { - let subjectString = `user ${state.getUsername()}`; - if (state.getUseBearerTokenForAmApis()) { - try { - const name = ( - await getServiceAccount({ - serviceAccountId: state.getServiceAccountId(), - state, - }) - ).name; - subjectString = `service account ${name} [${state.getServiceAccountId()}]`; - } catch (error) { - subjectString = `service account ${state.getServiceAccountId()}`; - } - } - return subjectString; -} - -/** - * Helper method to set, reset, or cancel timer to auto refresh tokens - * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) - * @param {boolean} autoRefresh true to automatically refresh tokens before they expire (default: true) - * @param {State} state library state - */ -function scheduleAutoRefresh( - forceLoginAsUser: boolean, - autoRefresh: boolean, - state: State -) { - let timer = state.getAutoRefreshTimer(); - // clear existing timer - if (timer) { - debugMessage({ - message: `AuthenticateOps.scheduleAutoRefresh: cancel existing timer`, - state, - }); - clearTimeout(timer); - } - // new timer - if (autoRefresh) { - const expires = - state.getDeploymentType() === Constants.CLASSIC_DEPLOYMENT_TYPE_KEY - ? state.getUserSessionTokenMeta()?.expires - : state.getUseBearerTokenForAmApis() - ? state.getBearerTokenMeta()?.expires - : Math.min( - state.getBearerTokenMeta()?.expires, - state.getUserSessionTokenMeta()?.expires - ); - let timeout = expires - Date.now() - 1000 * 25; - if (timeout < 1000 * 30) { - debugMessage({ - message: `Timeout below threshold of 30 seconds (${Math.ceil( - timeout / 1000 - )}), resetting timeout to 10ms.`, - state, - }); - if (timeout < 10) timeout = 10; - } - debugMessage({ - message: `AuthenticateOps.scheduleAutoRefresh: set new timer [${Math.floor( - timeout / 1000 - )}s (${new Date(timeout).getMinutes()}m ${new Date( - timeout - ).getSeconds()}s)]`, - state, - }); - timer = setTimeout(getTokens, timeout, { - forceLoginAsUser, - autoRefresh, - state, - // Volker's Visual Studio Code doesn't want to have it any other way. - }) as unknown as NodeJS.Timeout; - state.setAutoRefreshTimer(timer); - timer.unref(); - } -} - -export type Tokens = { - bearerToken?: AccessTokenMetaType; - userSessionToken?: UserSessionMetaType; - subject?: string; - host?: string; - realm?: string; -}; - -/** - * Get tokens - * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) - * @param {boolean} autoRefresh true to automatically refresh tokens before they expire (default: true) - * @param {State} state library state - * @returns {Promise} object containing the tokens - */ -export async function getTokens({ - forceLoginAsUser = process.env.FRODO_FORCE_LOGIN_AS_USER ? true : false, - autoRefresh = true, - types = Constants.DEPLOYMENT_TYPES, - callbackHandler = null, - state, -}: { - forceLoginAsUser?: boolean; - autoRefresh?: boolean; - types?: string[]; - callbackHandler?: CallbackHandler; - state: State; -}): Promise { - debugMessage({ - message: `AuthenticateOps.getTokens: start, types: ${types}`, - state, - }); - if (!state.getHost()) { - throw new FrodoError(`No host specified`); - } - let usingConnectionProfile: boolean = false; - try { - // if username/password on cli are empty, try to read from connections.json - if ( - state.getUsername() == null && - state.getPassword() == null && - !state.getServiceAccountId() && - !state.getServiceAccountJwk() - ) { - usingConnectionProfile = await loadConnectionProfile({ state }); - - // fail fast if deployment type not applicable - if ( - state.getDeploymentType() && - !types.includes(state.getDeploymentType()) - ) { - throw new FrodoError( - `Unsupported deployment type '${state.getDeploymentType()}'` - ); - } - } - - // if host is not a valid URL, try to locate a valid URL and deployment type from connections.json - if (!isValidUrl(state.getHost())) { - const conn = await getConnectionProfile({ state }); - state.setHost(conn.tenant); - state.setAllowInsecureConnection(conn.allowInsecureConnection); - state.setDeploymentType(conn.deploymentType); - - // fail fast if deployment type not applicable - if ( - state.getDeploymentType() && - !types.includes(state.getDeploymentType()) - ) { - throw new FrodoError( - `Unsupported deployment type '${state.getDeploymentType()}'` - ); - } - } - - // now that we have the full tenant URL we can lookup the cookie name - state.setCookieName(await determineCookieName(state)); - - // use service account to login? - if ( - !forceLoginAsUser && - (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || - state.getDeploymentType() === undefined) && - state.getServiceAccountId() && - state.getServiceAccountJwk() - ) { - debugMessage({ - message: `AuthenticateOps.getTokens: Authenticating with service account ${state.getServiceAccountId()}`, - state, - }); - try { - const token = await getSaBearerToken({ state }); - if (token) state.setBearerTokenMeta(token); - if (usingConnectionProfile && !token.from_cache) { - saveConnectionProfile({ host: state.getHost(), state }); - } - state.setUseBearerTokenForAmApis(true); - await determineDeploymentTypeAndDefaultRealmAndVersion(state); - - // fail if deployment type not applicable - if ( - state.getDeploymentType() && - !types.includes(state.getDeploymentType()) - ) { - throw new FrodoError( - `Unsupported deployment type: '${state.getDeploymentType()}' not in ${types}` - ); - } - } catch (saErr) { - throw new FrodoError(`Service account login error`, saErr); - } - } - // use user account to login - else if (state.getUsername() && state.getPassword()) { - debugMessage({ - message: `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`, - state, - }); - const token = await getUserSessionToken(callbackHandler, state); - if (token) state.setUserSessionTokenMeta(token); - if (usingConnectionProfile && !token.from_cache) { - saveConnectionProfile({ host: state.getHost(), state }); - } - await determineDeploymentTypeAndDefaultRealmAndVersion(state); - - // fail if deployment type not applicable - if ( - state.getDeploymentType() && - !types.includes(state.getDeploymentType()) - ) { - throw new FrodoError( - `Unsupported deployment type '${state.getDeploymentType()}'` - ); - } - - if ( - state.getCookieValue() && - // !state.getBearerToken() && - (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || - state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) - ) { - const accessToken = await getUserBearerToken(state); - if (accessToken) state.setBearerTokenMeta(accessToken); - } - } - // incomplete or no credentials - else { - throw new FrodoError(`Incomplete or no credentials`); - } - if ( - state.getCookieValue() || - (state.getUseBearerTokenForAmApis() && state.getBearerToken()) - ) { - if (state.getBearerTokenMeta()?.from_cache) { - verboseMessage({ message: `Using cached bearer token.`, state }); - } - if ( - !state.getUseBearerTokenForAmApis() && - state.getUserSessionTokenMeta()?.from_cache - ) { - verboseMessage({ message: `Using cached session token.`, state }); - } - scheduleAutoRefresh(forceLoginAsUser, autoRefresh, state); - const tokens: Tokens = { - bearerToken: state.getBearerTokenMeta(), - userSessionToken: state.getUserSessionTokenMeta(), - subject: await getLoggedInSubject(state), - host: state.getHost(), - realm: state.getRealm() ? state.getRealm() : 'root', - }; - debugMessage({ - message: `AuthenticateOps.getTokens: end with tokens`, - state, - }); - return tokens; - } - } catch (error) { - throw new FrodoError(`Error getting tokens`, error); - } -} diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index 3ba5d8e6f..9b0ac779c 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -335,12 +335,6 @@ async function determineDeploymentType(state: State): Promise { return deploymentType; case Constants.CLASSIC_DEPLOYMENT_TYPE_KEY: - debugMessage({ - message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, - state, - }); - return deploymentType; - case Constants.IDM_DEPLOYMENT_TYPE_KEY: debugMessage({ message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, @@ -384,7 +378,6 @@ async function determineDeploymentType(state: State): Promise { state, }); } catch (e) { - // debugMessage(e.response); // If error is in that condition after sending Authorize if ( e.response?.status === 302 && @@ -1137,14 +1130,13 @@ export async function getTokens({ ); } } - //username and password empty if ended - // if host is not a valid URL, try to locate a valid URL and deployment type from connections.json if (!isValidUrl(state.getHost())) { const conn = await getConnectionProfile({ state }); state.setHost(conn.tenant); state.setAllowInsecureConnection(conn.allowInsecureConnection); state.setDeploymentType(conn.deploymentType); + // fail fast if deployment type not applicable if ( state.getDeploymentType() && diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index 93e89c1ee..ba692900d 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -351,8 +351,9 @@ export async function exportFullConfiguration({ const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; + let config = {} as ConfigEntityExportInterface; - if (!isIdmDeployment) { + if (isPlatformDeployment || isClassicDeployment) { config = await exportAmConfigEntities({ includeReadOnly, onlyRealm, @@ -477,7 +478,7 @@ export async function exportFullConfiguration({ exportServices, globalStateObj, errors, - !isIdmDeployment + isPlatformDeployment || isClassicDeployment ) )?.service, site: ( @@ -508,7 +509,7 @@ export async function exportFullConfiguration({ Object.keys(globalConfig.idm) .filter( (k) => - (k === 'ui/themerealm' && !isIdmDeployment) || + (k === 'ui/themerealm' && isPlatformDeployment || isClassicDeployment) || k === 'sync' || k.startsWith('mapping/') || k.startsWith('emailTemplate/') @@ -518,7 +519,7 @@ export async function exportFullConfiguration({ } const realmConfig = {}; - if (!isIdmDeployment && (!onlyGlobal || onlyRealm)) { + if (isPlatformDeployment || isClassicDeployment && (!onlyGlobal || onlyRealm)) { // Export realm configs const activeRealm = state.getRealm(); for (const realm of Object.keys(config.realm)) { @@ -914,7 +915,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'Services', - !isIdmDeployment && !!importData.global.service + isPlatformDeployment || isClassicDeployment && !!importData.global.service ) ); response.push( @@ -961,7 +962,7 @@ export async function importFullConfiguration({ status: 'success', state, }); - if (!isIdmDeployment) { + if (isPlatformDeployment || isClassicDeployment) { // Import to realms const currentRealm = state.getRealm(); for (const realm of Object.keys(importData.realm)) { diff --git a/src/shared/Constants.ts b/src/shared/Constants.ts index 0471c5c91..f75045ad6 100644 --- a/src/shared/Constants.ts +++ b/src/shared/Constants.ts @@ -25,7 +25,7 @@ const DEPLOYMENT_TYPES = [ CLASSIC_DEPLOYMENT_TYPE_KEY, CLOUD_DEPLOYMENT_TYPE_KEY, FORGEOPS_DEPLOYMENT_TYPE_KEY, - IDM_DEPLOYMENT_TYPE_KEY, + IDM_DEPLOYMENT_TYPE_KEY ]; const DEPLOYMENT_TYPE_REALM_MAP = { [CLASSIC_DEPLOYMENT_TYPE_KEY]: '/', From de6b8e56248459e7bad45b93888d9dde48191a9c Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Thu, 15 May 2025 11:03:08 -0600 Subject: [PATCH 5/9] Made changes on parenthesis --- src/ops/ConfigOps.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index ba692900d..73445d538 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -351,7 +351,7 @@ export async function exportFullConfiguration({ const isPlatformDeployment = isCloudDeployment || isForgeOpsDeployment; const isIdmDeployment = state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY; - + let config = {} as ConfigEntityExportInterface; if (isPlatformDeployment || isClassicDeployment) { config = await exportAmConfigEntities({ @@ -509,7 +509,8 @@ export async function exportFullConfiguration({ Object.keys(globalConfig.idm) .filter( (k) => - (k === 'ui/themerealm' && isPlatformDeployment || isClassicDeployment) || + (k === 'ui/themerealm' && isPlatformDeployment) || + isClassicDeployment || k === 'sync' || k.startsWith('mapping/') || k.startsWith('emailTemplate/') @@ -519,7 +520,10 @@ export async function exportFullConfiguration({ } const realmConfig = {}; - if (isPlatformDeployment || isClassicDeployment && (!onlyGlobal || onlyRealm)) { + if ( + (isPlatformDeployment || isClassicDeployment) && + (!onlyGlobal || onlyRealm) + ) { // Export realm configs const activeRealm = state.getRealm(); for (const realm of Object.keys(config.realm)) { @@ -915,7 +919,8 @@ export async function importFullConfiguration({ errors, indicatorId, 'Services', - isPlatformDeployment || isClassicDeployment && !!importData.global.service + (isPlatformDeployment || isClassicDeployment) && + !!importData.global.service ) ); response.push( From b0ff7e59ba531f753c07eaa6cd2a07c7c3177342 Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Wed, 4 Jun 2025 08:42:45 -0600 Subject: [PATCH 6/9] changed authenticateOps.ts --- src/api/BaseApi.ts | 2 +- src/ops/AuthenticateOps.ts | 125 ++++++++++++++++++++----------------- src/shared/Constants.ts | 2 +- 3 files changed, 70 insertions(+), 59 deletions(-) diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index 090fddfd1..9b4ba5848 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -323,7 +323,7 @@ export function generateIdmApi({ }, requestOverride ); - + const request = createAxiosInstance(state, requestConfig); // enable curlirizer output in debug mode diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index 9b0ac779c..6d9c9fec6 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -155,12 +155,23 @@ let adminClientId = fidcClientId; * @returns {string} cookie name */ async function determineCookieName(state: State): Promise { - const data = await getServerInfo({ state }); + let cookieName = null; + try { + const data = await getServerInfo({ state }); + cookieName = data.cookieName; + } catch (e) { + if ( + e.response?.status !== 401 || + e.response?.data.message !== 'Access Denied' + ) { + throw e; + } + } debugMessage({ - message: `AuthenticateOps.determineCookieName: cookieName=${data.cookieName}`, + message: `AuthenticateOps.determineCookieName: cookieName=${cookieName}`, state, }); - return data.cookieName; + return cookieName; } /** @@ -378,7 +389,7 @@ async function determineDeploymentType(state: State): Promise { state, }); } catch (e) { - // If error is in that condition after sending Authorize + // debugMessage(e.response); if ( e.response?.status === 302 && e.response.headers?.location?.indexOf('code=') > -1 @@ -411,16 +422,11 @@ async function determineDeploymentType(state: State): Promise { deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; } else { try { - //I need to check if it is idm here const idmresponse = await stepIdm({ body: {}, config: {}, state, }); - verboseMessage({ - message: `idm response = ${JSON.stringify(idmresponse.status, null, 2)} + ${idmresponse.data.authorization.authLogin}`, - state, - }); if ( idmresponse.status === 200 && idmresponse.data?.authorization.authLogin @@ -432,12 +438,11 @@ async function determineDeploymentType(state: State): Promise { state, }); deploymentType = Constants.IDM_DEPLOYMENT_TYPE_KEY; + } else { verboseMessage({ - message: 'deployment type in determine =' + deploymentType, + message: `Classic deployment`['brightCyan'] + ` detected.`, state, }); - } else { - throw new Error('Not IDM'); } } catch { verboseMessage({ @@ -453,7 +458,6 @@ async function determineDeploymentType(state: State): Promise { message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, state, }); - return deploymentType; } } @@ -504,7 +508,18 @@ async function getFreshUserSessionToken({ 'X-OpenAM-Password': state.getPassword(), }, }; - let response = await step({ body: {}, config, state }); + let response; + try { + response = await step({ body: {}, config, state }); + } catch (e) { + if ( + e.response?.status !== 401 || + e.response?.data.message !== 'Access Denied' + ) { + throw e; + } + return null; + } let skip2FA = null; let steps = 0; @@ -586,6 +601,7 @@ async function getUserSessionToken( otpCallbackHandler: otpCallback, state, }); + if (!token) return token; token.from_cache = false; debugMessage({ message: `AuthenticateOps.getUserSessionToken: fresh`, @@ -973,6 +989,7 @@ async function determineDeploymentTypeAndDefaultRealmAndVersion( state, }); state.setDeploymentType(await determineDeploymentType(state)); + if (state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY) return; determineDefaultRealm(state); debugMessage({ message: `AuthenticateOps.determineDeploymentTypeAndDefaultRealmAndVersion: realm=${state.getRealm()}, type=${state.getDeploymentType()}`, @@ -1082,6 +1099,7 @@ export type Tokens = { host?: string; realm?: string; }; + /** * Get tokens * @param {boolean} forceLoginAsUser true to force login as user even if a service account is available (default: false) @@ -1130,13 +1148,14 @@ export async function getTokens({ ); } } + // if host is not a valid URL, try to locate a valid URL and deployment type from connections.json if (!isValidUrl(state.getHost())) { const conn = await getConnectionProfile({ state }); state.setHost(conn.tenant); state.setAllowInsecureConnection(conn.allowInsecureConnection); state.setDeploymentType(conn.deploymentType); - + // fail fast if deployment type not applicable if ( state.getDeploymentType() && @@ -1147,14 +1166,10 @@ export async function getTokens({ ); } } - if (state.getHost().endsWith('openidm')) { - state.setDeploymentType(await determineDeploymentType(state)); - } - //check if it is idm deployment type, then it will just do some stuff for idm and break - else { - // now that we have the full tenant URL we can lookup the cookie name - state.setCookieName(await determineCookieName(state)); - } + + // now that we have the full tenant URL we can lookup the cookie name + state.setCookieName(await determineCookieName(state)); + // use service account to login? if ( !forceLoginAsUser && @@ -1195,49 +1210,45 @@ export async function getTokens({ message: `AuthenticateOps.getTokens: Authenticating with user account ${state.getUsername()}`, state, }); - // if logging into on prem idm - if (state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY) { - const token: Tokens = { - subject: state.getUsername(), - host: state.getHost(), - realm: state.getRealm() ? state.getRealm() : 'root', - }; + const token = await getUserSessionToken(callbackHandler, state); + if (token) state.setUserSessionTokenMeta(token); + if (usingConnectionProfile && (!token || !token.from_cache)) { saveConnectionProfile({ host: state.getHost(), state }); - return token; - } else { - const token = await getUserSessionToken(callbackHandler, state); - if (token) state.setUserSessionTokenMeta(token); - if (usingConnectionProfile && !token.from_cache) { - saveConnectionProfile({ host: state.getHost(), state }); - } - await determineDeploymentTypeAndDefaultRealmAndVersion(state); + } + await determineDeploymentTypeAndDefaultRealmAndVersion(state); - // fail if deployment type not applicable - if ( - state.getDeploymentType() && - !types.includes(state.getDeploymentType()) - ) { - throw new FrodoError( - `Unsupported deployment type '${state.getDeploymentType()}'` - ); - } + // fail if deployment type not applicable + if ( + state.getDeploymentType() && + !types.includes(state.getDeploymentType()) + ) { + throw new FrodoError( + `Unsupported deployment type '${state.getDeploymentType()}'` + ); + } - if ( - state.getCookieValue() && - // !state.getBearerToken() && - (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || - state.getDeploymentType() === - Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) - ) { - const accessToken = await getUserBearerToken(state); - if (accessToken) state.setBearerTokenMeta(accessToken); - } + if ( + state.getCookieValue() && + // !state.getBearerToken() && + (state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY || + state.getDeploymentType() === Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY) + ) { + const accessToken = await getUserBearerToken(state); + if (accessToken) state.setBearerTokenMeta(accessToken); } } // incomplete or no credentials else { throw new FrodoError(`Incomplete or no credentials`); } + // Return IDM tokens for IDM deployment type + if (state.getDeploymentType() === Constants.IDM_DEPLOYMENT_TYPE_KEY) { + return { + subject: state.getUsername(), + host: state.getHost(), + realm: state.getRealm() ? state.getRealm() : 'root', + }; + } if ( state.getCookieValue() || (state.getUseBearerTokenForAmApis() && state.getBearerToken()) diff --git a/src/shared/Constants.ts b/src/shared/Constants.ts index f75045ad6..0471c5c91 100644 --- a/src/shared/Constants.ts +++ b/src/shared/Constants.ts @@ -25,7 +25,7 @@ const DEPLOYMENT_TYPES = [ CLASSIC_DEPLOYMENT_TYPE_KEY, CLOUD_DEPLOYMENT_TYPE_KEY, FORGEOPS_DEPLOYMENT_TYPE_KEY, - IDM_DEPLOYMENT_TYPE_KEY + IDM_DEPLOYMENT_TYPE_KEY, ]; const DEPLOYMENT_TYPE_REALM_MAP = { [CLASSIC_DEPLOYMENT_TYPE_KEY]: '/', From f2144526b49d2f95c8675ffd9a11f26a2fff07fa Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Wed, 4 Jun 2025 16:38:40 -0600 Subject: [PATCH 7/9] Changed includeReadOnly --- src/ops/ConfigOps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index 73445d538..5ddff7d13 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -445,7 +445,7 @@ export async function exportFullConfiguration({ exportRealms, stateObj, errors, - includeReadOnly || isClassicDeployment + (includeReadOnly && isPlatformDeployment) || isClassicDeployment ) )?.realm, scripttype: ( @@ -453,7 +453,7 @@ export async function exportFullConfiguration({ exportScriptTypes, stateObj, errors, - includeReadOnly || isClassicDeployment + (includeReadOnly && isPlatformDeployment) || isClassicDeployment ) )?.scripttype, secret: ( From c6dda67427ad5d7a4185c1166b08932423071484 Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Wed, 24 Sep 2025 16:27:24 -0600 Subject: [PATCH 8/9] Added AuthenticationApi test for on-prem idm Changed stepIdm to authenticateIdm other fixes based on the PR review. --- src/api/AuthenticateApi.test.ts | 26 +++ src/api/AuthenticateApi.ts | 4 +- src/ops/AuthenticateOps.ts | 27 ++- .../recording.har | 175 ++++++++++++++++++ 4 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har diff --git a/src/api/AuthenticateApi.test.ts b/src/api/AuthenticateApi.test.ts index 3f94dcf31..1bf8ff743 100644 --- a/src/api/AuthenticateApi.test.ts +++ b/src/api/AuthenticateApi.test.ts @@ -116,4 +116,30 @@ describe('AuthenticateApi', () => { expect(response2).toMatchSnapshot(); }); }); + + describe('authenticateIdm()', () => { + test('0: Method is implemented', async () => { + expect(AuthenticateApi.step).toBeDefined(); + }); + + test('1: On-prem IDM authentication', async () => { + state.setHost( + process.env.FRODO_HOST || 'http://openidm-frodo-dev.classic.com:9080/openidm' + ); + state.setUsername(process.env.FRODO_USERNAME || 'openidm-admin'); + state.setPassword(process.env.FRODO_PASSWORD || 'openidm-admin'); + const config = { + headers: { + 'X-OpenAM-Username': state.getUsername(), + 'X-OpenAM-Password': state.getPassword(), + }, + }; + const response1 = await AuthenticateApi.authenticateIdm({ + body: {}, + config, + state, + }); + expect(response1.status).toEqual(200); + }); + }); }); diff --git a/src/api/AuthenticateApi.ts b/src/api/AuthenticateApi.ts index b3f92535d..c3d75a565 100644 --- a/src/api/AuthenticateApi.ts +++ b/src/api/AuthenticateApi.ts @@ -81,7 +81,7 @@ export async function step({ * @param {any} config request config * @returns Promise resolving to the authentication service response */ -export async function stepIdm({ +export async function authenticateIdm({ body = {}, config = {}, state, @@ -93,7 +93,7 @@ export async function stepIdm({ state: State; }): Promise { debugMessage({ - message: `AuthenticateApi.stepIdm: function start `, + message: `AuthenticateApi.authenticateIdm: function start `, state, }); const urlString = `${state.getHost()}/authentication?_action=login`; diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index 6d9c9fec6..ed2be854d 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'crypto'; import url from 'url'; import { v4 } from 'uuid'; -import { step, stepIdm } from '../api/AuthenticateApi'; +import { authenticateIdm, step } from '../api/AuthenticateApi'; import { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi'; import Constants from '../shared/Constants'; import { State } from '../shared/State'; @@ -422,7 +422,7 @@ async function determineDeploymentType(state: State): Promise { deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; } else { try { - const idmresponse = await stepIdm({ + const idmresponse = await authenticateIdm({ body: {}, config: {}, state, @@ -444,11 +444,22 @@ async function determineDeploymentType(state: State): Promise { state, }); } - } catch { - verboseMessage({ - message: `Classic deployment`['brightCyan'] + ` detected.`, - state, - }); + } catch (e: any) { + if ( + e.response?.status !== 401 || + e.response?.data.message !== 'Access Denied' + ) { + debugMessage({ + message: `AuthenticateOps: 401 Unauthorized received – credentials may be invalid but IDM deployment is still possible.`, + state, + }); + throw e; + } else { + verboseMessage({ + message: `Classic deployment`['brightCyan'] + ` detected.`, + state, + }); + } } } } @@ -601,7 +612,7 @@ async function getUserSessionToken( otpCallbackHandler: otpCallback, state, }); - if (!token) return token; + if (!token) return null; token.from_cache = false; debugMessage({ message: `AuthenticateOps.getUserSessionToken: fresh`, diff --git a/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har b/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har new file mode 100644 index 000000000..0f0413ad8 --- /dev/null +++ b/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har @@ -0,0 +1,175 @@ +{ + "log": { + "_recordingName": "AuthenticateApi/authenticateIdm()/1: On-prem IDM authentication", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "6.0.6" + }, + "entries": [ + { + "_id": "a54f8002c42635eab99b513f957d9a65", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 2, + "cookies": [], + "headers": [ + { + "name": "accept", + "value": "application/json, text/plain, */*" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "user-agent", + "value": "@rockcarver/frodo-lib/3.1.0" + }, + { + "name": "x-forgerock-transactionid", + "value": "frodo-72889b06-1000-480e-833f-52222f508963" + }, + { + "name": "x-openidm-username", + "value": "openidm-admin" + }, + { + "name": "x-openidm-password", + "value": "openidm-admin" + }, + { + "name": "x-openam-username", + "value": "openidm-admin" + }, + { + "name": "x-openam-password", + "value": "openidm-admin" + }, + { + "name": "content-length", + "value": "2" + }, + { + "name": "accept-encoding", + "value": "gzip, compress, deflate, br" + }, + { + "name": "host", + "value": "openidm-frodo-dev.classic.com:9080" + } + ], + "headersSize": 528, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "{}" + }, + "queryString": [ + { + "name": "_action", + "value": "login" + } + ], + "url": "http://openidm-frodo-dev.classic.com:9080/openidm/authentication?_action=login" + }, + "response": { + "bodySize": 326, + "content": { + "mimeType": "application/json;charset=utf-8", + "size": 326, + "text": "{\"_id\":\"login\",\"authorization\":{\"userRolesProperty\":\"authzRoles\",\"component\":\"internal/user\",\"authLogin\":true,\"roles\":[\"internal/role/openidm-admin\",\"internal/role/openidm-authorized\"],\"ipAddress\":\"127.0.0.1\",\"authenticationId\":\"openidm-admin\",\"id\":\"openidm-admin\",\"moduleId\":\"STATIC_USER\"},\"authenticationId\":\"openidm-admin\"}" + }, + "cookies": [ + { + "httpOnly": true, + "name": "session-jwt", + "path": "/", + "value": "eyJ0eXAiOiJKV1QiLCJraWQiOiJvcGVuaWRtLWp3dHNlc3Npb25obWFjLWtleSIsImN0eSI6IkpXVCIsImFsZyI6IkhTMjU2In0.ZXlKMGVYQWlPaUpLVjFRaUxDSnJhV1FpT2lKdmNHVnVhV1J0TFd4dlkyRnNhRzl6ZENJc0ltVnVZeUk2SWtFeE1qaERRa010U0ZNeU5UWWlMQ0poYkdjaU9pSlNVMEV4WHpVaWZRLmx4UkRwZnp0ODIzSTRQYy1mdnR0ckJtekNDbXBURVM1WmE4QktkUExOLTExMTIzYmplMUxBU0xfOVN4bUUtdDhIbzluc01zeVo4UWlBQkxVVExWbHJ0S08zUGVVY2ozdW0yVURxM3hkYUJlTUlVdm9LZkxHR3o1N0Zqb3VpdWlvN1ZNLW1jelc4blpJR2lrY24zRWxwdFBhZmdQcHdNV1VXSWlFN2kxM2V6Ml9UTmdibHN6ZEhyVURSdVhpdFFra2pzMVZadzhaS05GeXpiRFo0TnRVWUhkdWFPRTlsS2tJOS1ZRHBYVnQtNk5pNV9jcHpuVXMwcnJ4SW9jOGxIMzhRUGZzRWRTaVQ3SXlEdUN2NmdjbzJTVkZ3S3I0aHUwTFd6azcwZFZLbXE0enpiX21qR0M5cWE1ano3QVJlRkpQakd1QWVwRUw4T0JsanM0VEZBQVlfQS5hLUQyVGJrNW1SMWU0REJPQ0Z1MVBnLnhRMGhQbXhtWkUzTGdxdjN2cWpiZ2NNZUlLd2RoeGpscDdRc1NaalktNjhxUEZIWFR4RnNjWWxad3k5UFJBZmdCMXVJc0c1dmVhdkFXZmUwX2pDdUpwV0pDYi16Y2FSTzdxN1J3Z1VRNGlYZy1JQy1INnpGQUp4ZG9OUVpnLVVVQTlvbXJqc3lORGtzWlJLS1VLOUlIb1V0NE9pMGJHSzB6Y0Y1SVZtQUQxN3ZPVGpzVEJxMXZHSlVJamxrcDNWaVowVXZSaXMyMDFJM3lMZVVxTDR6emlvd2RPemJYZ21KQUQ1XzdHbUpfN1JXTjJUZlNTNWIyN1VKMGhzRTdac3RTN2lxa2k0Ujd5eEl2TUFaZ3BVZ0hsU0NVdFJVangzQXBDS1ZKSFY1UVdrbGlWbTEtNjg4cWdXTVpOZnEzTFlFUTl5cUhfWGtVREdLLWhJdzA4Z3dSdy12SFlWUUh6TkxxdDZyR3dFWnRobkh3RzlXOXY4Q3VvbHpOaHotUlZYWlJ5cmVhdzFPVUxnOEJuemRkUktYblJXT1ZlUGJxWDN3TEdhbnlyUHdPRkRIa0tEWE5ZTjhHbkNlbmF4RmQwa1dqOHZBbVFnWVVYZE5GaTIzME9JX2pFNUNERzNuV1BVOGJKZ2JjZS0xdWY5MnE3Xzd2Z0ROV1Y5QkNSSHBWYlBtRFZudVdnQVUzWVlSX0dyTjBlZ2w5aVpydnh1X0dGSXA5SThaWDZYY21kRGRSMGtwelV5MmlWNnBCQWJGMGhLU2tpTndXc3dHUFhzSkZOc25qZ2lQY3pLTkpTYkI0dmpqVGNxSkZ0UldSQW1nNW9jTmlKckRJc2lvaXFxamhJelJhbF81WE8wLThfN2tyOVF1RG1CUWpjdVVYRHFFWEtzZmk4elJiYms0ZmY4ZWVlcFRMQ3kyTTN2MF9laTRnazJSQndPVHYyTU43d1BfbFNLSGg4YVRkQS54ZVNfRU1ESzdjczA4anVGSlI0emNR.wNg2zdjbr3v3DA2z_ljNV-FK-2LkXrxuRdKzlkVNqeU" + } + ], + "headers": [ + { + "name": "date", + "value": "Tue, 23 Sep 2025 20:43:40 GMT" + }, + { + "name": "vary", + "value": "Accept-Encoding, Origin" + }, + { + "name": "cache-control", + "value": "no-store" + }, + { + "name": "content-api-version", + "value": "protocol=2.1,resource=1.0" + }, + { + "name": "content-security-policy", + "value": "default-src 'none';frame-ancestors 'none';sandbox" + }, + { + "name": "content-type", + "value": "application/json;charset=utf-8" + }, + { + "name": "cross-origin-opener-policy", + "value": "same-origin" + }, + { + "name": "cross-origin-resource-policy", + "value": "same-origin" + }, + { + "name": "expires", + "value": "0" + }, + { + "name": "pragma", + "value": "no-cache" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "session-jwt=eyJ0eXAiOiJKV1QiLCJraWQiOiJvcGVuaWRtLWp3dHNlc3Npb25obWFjLWtleSIsImN0eSI6IkpXVCIsImFsZyI6IkhTMjU2In0.ZXlKMGVYQWlPaUpLVjFRaUxDSnJhV1FpT2lKdmNHVnVhV1J0TFd4dlkyRnNhRzl6ZENJc0ltVnVZeUk2SWtFeE1qaERRa010U0ZNeU5UWWlMQ0poYkdjaU9pSlNVMEV4WHpVaWZRLmx4UkRwZnp0ODIzSTRQYy1mdnR0ckJtekNDbXBURVM1WmE4QktkUExOLTExMTIzYmplMUxBU0xfOVN4bUUtdDhIbzluc01zeVo4UWlBQkxVVExWbHJ0S08zUGVVY2ozdW0yVURxM3hkYUJlTUlVdm9LZkxHR3o1N0Zqb3VpdWlvN1ZNLW1jelc4blpJR2lrY24zRWxwdFBhZmdQcHdNV1VXSWlFN2kxM2V6Ml9UTmdibHN6ZEhyVURSdVhpdFFra2pzMVZadzhaS05GeXpiRFo0TnRVWUhkdWFPRTlsS2tJOS1ZRHBYVnQtNk5pNV9jcHpuVXMwcnJ4SW9jOGxIMzhRUGZzRWRTaVQ3SXlEdUN2NmdjbzJTVkZ3S3I0aHUwTFd6azcwZFZLbXE0enpiX21qR0M5cWE1ano3QVJlRkpQakd1QWVwRUw4T0JsanM0VEZBQVlfQS5hLUQyVGJrNW1SMWU0REJPQ0Z1MVBnLnhRMGhQbXhtWkUzTGdxdjN2cWpiZ2NNZUlLd2RoeGpscDdRc1NaalktNjhxUEZIWFR4RnNjWWxad3k5UFJBZmdCMXVJc0c1dmVhdkFXZmUwX2pDdUpwV0pDYi16Y2FSTzdxN1J3Z1VRNGlYZy1JQy1INnpGQUp4ZG9OUVpnLVVVQTlvbXJqc3lORGtzWlJLS1VLOUlIb1V0NE9pMGJHSzB6Y0Y1SVZtQUQxN3ZPVGpzVEJxMXZHSlVJamxrcDNWaVowVXZSaXMyMDFJM3lMZVVxTDR6emlvd2RPemJYZ21KQUQ1XzdHbUpfN1JXTjJUZlNTNWIyN1VKMGhzRTdac3RTN2lxa2k0Ujd5eEl2TUFaZ3BVZ0hsU0NVdFJVangzQXBDS1ZKSFY1UVdrbGlWbTEtNjg4cWdXTVpOZnEzTFlFUTl5cUhfWGtVREdLLWhJdzA4Z3dSdy12SFlWUUh6TkxxdDZyR3dFWnRobkh3RzlXOXY4Q3VvbHpOaHotUlZYWlJ5cmVhdzFPVUxnOEJuemRkUktYblJXT1ZlUGJxWDN3TEdhbnlyUHdPRkRIa0tEWE5ZTjhHbkNlbmF4RmQwa1dqOHZBbVFnWVVYZE5GaTIzME9JX2pFNUNERzNuV1BVOGJKZ2JjZS0xdWY5MnE3Xzd2Z0ROV1Y5QkNSSHBWYlBtRFZudVdnQVUzWVlSX0dyTjBlZ2w5aVpydnh1X0dGSXA5SThaWDZYY21kRGRSMGtwelV5MmlWNnBCQWJGMGhLU2tpTndXc3dHUFhzSkZOc25qZ2lQY3pLTkpTYkI0dmpqVGNxSkZ0UldSQW1nNW9jTmlKckRJc2lvaXFxamhJelJhbF81WE8wLThfN2tyOVF1RG1CUWpjdVVYRHFFWEtzZmk4elJiYms0ZmY4ZWVlcFRMQ3kyTTN2MF9laTRnazJSQndPVHYyTU43d1BfbFNLSGg4YVRkQS54ZVNfRU1ESzdjczA4anVGSlI0emNR.wNg2zdjbr3v3DA2z_ljNV-FK-2LkXrxuRdKzlkVNqeU; Path=/; HttpOnly" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-frame-options", + "value": "DENY" + }, + { + "name": "content-length", + "value": "326" + } + ], + "headersSize": 2268, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2025-09-23T20:43:39.972Z", + "time": 94, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 94 + } + } + ], + "pages": [], + "version": "1.2" + } +} From cb11e1893d358f3d1215f13eb940d8427c69d1e9 Mon Sep 17 00:00:00 2001 From: Sean Koo Date: Mon, 10 Nov 2025 11:45:19 -0700 Subject: [PATCH 9/9] Modified AuthenticateApi.test made a new record for the test --- src/api/AuthenticateApi.test.ts | 7 +++-- src/api/AuthenticateApi.ts | 4 +-- src/ops/AuthenticateOps.ts | 3 +- .../recording.har | 28 ++++++++----------- .../api/AuthenticateApi.test.js.snap | 20 +++++++++++++ 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/api/AuthenticateApi.test.ts b/src/api/AuthenticateApi.test.ts index 1bf8ff743..769bbae68 100644 --- a/src/api/AuthenticateApi.test.ts +++ b/src/api/AuthenticateApi.test.ts @@ -130,8 +130,8 @@ describe('AuthenticateApi', () => { state.setPassword(process.env.FRODO_PASSWORD || 'openidm-admin'); const config = { headers: { - 'X-OpenAM-Username': state.getUsername(), - 'X-OpenAM-Password': state.getPassword(), + 'X-OpenIDM-Username': state.getUsername(), + 'X-OpenIDM-Password': state.getPassword(), }, }; const response1 = await AuthenticateApi.authenticateIdm({ @@ -139,7 +139,8 @@ describe('AuthenticateApi', () => { config, state, }); - expect(response1.status).toEqual(200); + expect(response1.authorization.authLogin).toBeTruthy(); + expect(response1).toMatchSnapshot(); }); }); }); diff --git a/src/api/AuthenticateApi.ts b/src/api/AuthenticateApi.ts index c3d75a565..5c84e52e0 100644 --- a/src/api/AuthenticateApi.ts +++ b/src/api/AuthenticateApi.ts @@ -97,8 +97,8 @@ export async function authenticateIdm({ state, }); const urlString = `${state.getHost()}/authentication?_action=login`; - const response = await generateIdmApi({ + const { data } = await generateIdmApi({ state, }).post(urlString, body, config); - return response; + return data; } diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index ed2be854d..94b1ce3e1 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -428,8 +428,7 @@ async function determineDeploymentType(state: State): Promise { state, }); if ( - idmresponse.status === 200 && - idmresponse.data?.authorization.authLogin + idmresponse.authorization.authLogin ) { verboseMessage({ message: diff --git a/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har b/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har index 0f0413ad8..5d7b01260 100644 --- a/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har +++ b/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har @@ -29,22 +29,18 @@ }, { "name": "x-forgerock-transactionid", - "value": "frodo-72889b06-1000-480e-833f-52222f508963" + "value": "frodo-a78b6567-d98d-477a-b45e-e4b5c545bed2" }, { - "name": "x-openidm-username", - "value": "openidm-admin" - }, - { - "name": "x-openidm-password", - "value": "openidm-admin" + "name": "authorization", + "value": "Bearer " }, { - "name": "x-openam-username", + "name": "x-openidm-username", "value": "openidm-admin" }, { - "name": "x-openam-password", + "name": "x-openidm-password", "value": "openidm-admin" }, { @@ -60,7 +56,7 @@ "value": "openidm-frodo-dev.classic.com:9080" } ], - "headersSize": 528, + "headersSize": 1982, "httpVersion": "HTTP/1.1", "method": "POST", "postData": { @@ -88,13 +84,13 @@ "httpOnly": true, "name": "session-jwt", "path": "/", - "value": "eyJ0eXAiOiJKV1QiLCJraWQiOiJvcGVuaWRtLWp3dHNlc3Npb25obWFjLWtleSIsImN0eSI6IkpXVCIsImFsZyI6IkhTMjU2In0.ZXlKMGVYQWlPaUpLVjFRaUxDSnJhV1FpT2lKdmNHVnVhV1J0TFd4dlkyRnNhRzl6ZENJc0ltVnVZeUk2SWtFeE1qaERRa010U0ZNeU5UWWlMQ0poYkdjaU9pSlNVMEV4WHpVaWZRLmx4UkRwZnp0ODIzSTRQYy1mdnR0ckJtekNDbXBURVM1WmE4QktkUExOLTExMTIzYmplMUxBU0xfOVN4bUUtdDhIbzluc01zeVo4UWlBQkxVVExWbHJ0S08zUGVVY2ozdW0yVURxM3hkYUJlTUlVdm9LZkxHR3o1N0Zqb3VpdWlvN1ZNLW1jelc4blpJR2lrY24zRWxwdFBhZmdQcHdNV1VXSWlFN2kxM2V6Ml9UTmdibHN6ZEhyVURSdVhpdFFra2pzMVZadzhaS05GeXpiRFo0TnRVWUhkdWFPRTlsS2tJOS1ZRHBYVnQtNk5pNV9jcHpuVXMwcnJ4SW9jOGxIMzhRUGZzRWRTaVQ3SXlEdUN2NmdjbzJTVkZ3S3I0aHUwTFd6azcwZFZLbXE0enpiX21qR0M5cWE1ano3QVJlRkpQakd1QWVwRUw4T0JsanM0VEZBQVlfQS5hLUQyVGJrNW1SMWU0REJPQ0Z1MVBnLnhRMGhQbXhtWkUzTGdxdjN2cWpiZ2NNZUlLd2RoeGpscDdRc1NaalktNjhxUEZIWFR4RnNjWWxad3k5UFJBZmdCMXVJc0c1dmVhdkFXZmUwX2pDdUpwV0pDYi16Y2FSTzdxN1J3Z1VRNGlYZy1JQy1INnpGQUp4ZG9OUVpnLVVVQTlvbXJqc3lORGtzWlJLS1VLOUlIb1V0NE9pMGJHSzB6Y0Y1SVZtQUQxN3ZPVGpzVEJxMXZHSlVJamxrcDNWaVowVXZSaXMyMDFJM3lMZVVxTDR6emlvd2RPemJYZ21KQUQ1XzdHbUpfN1JXTjJUZlNTNWIyN1VKMGhzRTdac3RTN2lxa2k0Ujd5eEl2TUFaZ3BVZ0hsU0NVdFJVangzQXBDS1ZKSFY1UVdrbGlWbTEtNjg4cWdXTVpOZnEzTFlFUTl5cUhfWGtVREdLLWhJdzA4Z3dSdy12SFlWUUh6TkxxdDZyR3dFWnRobkh3RzlXOXY4Q3VvbHpOaHotUlZYWlJ5cmVhdzFPVUxnOEJuemRkUktYblJXT1ZlUGJxWDN3TEdhbnlyUHdPRkRIa0tEWE5ZTjhHbkNlbmF4RmQwa1dqOHZBbVFnWVVYZE5GaTIzME9JX2pFNUNERzNuV1BVOGJKZ2JjZS0xdWY5MnE3Xzd2Z0ROV1Y5QkNSSHBWYlBtRFZudVdnQVUzWVlSX0dyTjBlZ2w5aVpydnh1X0dGSXA5SThaWDZYY21kRGRSMGtwelV5MmlWNnBCQWJGMGhLU2tpTndXc3dHUFhzSkZOc25qZ2lQY3pLTkpTYkI0dmpqVGNxSkZ0UldSQW1nNW9jTmlKckRJc2lvaXFxamhJelJhbF81WE8wLThfN2tyOVF1RG1CUWpjdVVYRHFFWEtzZmk4elJiYms0ZmY4ZWVlcFRMQ3kyTTN2MF9laTRnazJSQndPVHYyTU43d1BfbFNLSGg4YVRkQS54ZVNfRU1ESzdjczA4anVGSlI0emNR.wNg2zdjbr3v3DA2z_ljNV-FK-2LkXrxuRdKzlkVNqeU" + "value": "" } ], "headers": [ { "name": "date", - "value": "Tue, 23 Sep 2025 20:43:40 GMT" + "value": "Mon, 10 Nov 2025 18:43:02 GMT" }, { "name": "vary", @@ -135,7 +131,7 @@ { "_fromType": "array", "name": "set-cookie", - "value": "session-jwt=eyJ0eXAiOiJKV1QiLCJraWQiOiJvcGVuaWRtLWp3dHNlc3Npb25obWFjLWtleSIsImN0eSI6IkpXVCIsImFsZyI6IkhTMjU2In0.ZXlKMGVYQWlPaUpLVjFRaUxDSnJhV1FpT2lKdmNHVnVhV1J0TFd4dlkyRnNhRzl6ZENJc0ltVnVZeUk2SWtFeE1qaERRa010U0ZNeU5UWWlMQ0poYkdjaU9pSlNVMEV4WHpVaWZRLmx4UkRwZnp0ODIzSTRQYy1mdnR0ckJtekNDbXBURVM1WmE4QktkUExOLTExMTIzYmplMUxBU0xfOVN4bUUtdDhIbzluc01zeVo4UWlBQkxVVExWbHJ0S08zUGVVY2ozdW0yVURxM3hkYUJlTUlVdm9LZkxHR3o1N0Zqb3VpdWlvN1ZNLW1jelc4blpJR2lrY24zRWxwdFBhZmdQcHdNV1VXSWlFN2kxM2V6Ml9UTmdibHN6ZEhyVURSdVhpdFFra2pzMVZadzhaS05GeXpiRFo0TnRVWUhkdWFPRTlsS2tJOS1ZRHBYVnQtNk5pNV9jcHpuVXMwcnJ4SW9jOGxIMzhRUGZzRWRTaVQ3SXlEdUN2NmdjbzJTVkZ3S3I0aHUwTFd6azcwZFZLbXE0enpiX21qR0M5cWE1ano3QVJlRkpQakd1QWVwRUw4T0JsanM0VEZBQVlfQS5hLUQyVGJrNW1SMWU0REJPQ0Z1MVBnLnhRMGhQbXhtWkUzTGdxdjN2cWpiZ2NNZUlLd2RoeGpscDdRc1NaalktNjhxUEZIWFR4RnNjWWxad3k5UFJBZmdCMXVJc0c1dmVhdkFXZmUwX2pDdUpwV0pDYi16Y2FSTzdxN1J3Z1VRNGlYZy1JQy1INnpGQUp4ZG9OUVpnLVVVQTlvbXJqc3lORGtzWlJLS1VLOUlIb1V0NE9pMGJHSzB6Y0Y1SVZtQUQxN3ZPVGpzVEJxMXZHSlVJamxrcDNWaVowVXZSaXMyMDFJM3lMZVVxTDR6emlvd2RPemJYZ21KQUQ1XzdHbUpfN1JXTjJUZlNTNWIyN1VKMGhzRTdac3RTN2lxa2k0Ujd5eEl2TUFaZ3BVZ0hsU0NVdFJVangzQXBDS1ZKSFY1UVdrbGlWbTEtNjg4cWdXTVpOZnEzTFlFUTl5cUhfWGtVREdLLWhJdzA4Z3dSdy12SFlWUUh6TkxxdDZyR3dFWnRobkh3RzlXOXY4Q3VvbHpOaHotUlZYWlJ5cmVhdzFPVUxnOEJuemRkUktYblJXT1ZlUGJxWDN3TEdhbnlyUHdPRkRIa0tEWE5ZTjhHbkNlbmF4RmQwa1dqOHZBbVFnWVVYZE5GaTIzME9JX2pFNUNERzNuV1BVOGJKZ2JjZS0xdWY5MnE3Xzd2Z0ROV1Y5QkNSSHBWYlBtRFZudVdnQVUzWVlSX0dyTjBlZ2w5aVpydnh1X0dGSXA5SThaWDZYY21kRGRSMGtwelV5MmlWNnBCQWJGMGhLU2tpTndXc3dHUFhzSkZOc25qZ2lQY3pLTkpTYkI0dmpqVGNxSkZ0UldSQW1nNW9jTmlKckRJc2lvaXFxamhJelJhbF81WE8wLThfN2tyOVF1RG1CUWpjdVVYRHFFWEtzZmk4elJiYms0ZmY4ZWVlcFRMQ3kyTTN2MF9laTRnazJSQndPVHYyTU43d1BfbFNLSGg4YVRkQS54ZVNfRU1ESzdjczA4anVGSlI0emNR.wNg2zdjbr3v3DA2z_ljNV-FK-2LkXrxuRdKzlkVNqeU; Path=/; HttpOnly" + "value": "session-jwt=; Path=/; HttpOnly" }, { "name": "x-content-type-options", @@ -156,8 +152,8 @@ "status": 200, "statusText": "OK" }, - "startedDateTime": "2025-09-23T20:43:39.972Z", - "time": 94, + "startedDateTime": "2025-11-10T18:43:02.111Z", + "time": 193, "timings": { "blocked": -1, "connect": -1, @@ -165,7 +161,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 94 + "wait": 193 } } ], diff --git a/src/test/snapshots/api/AuthenticateApi.test.js.snap b/src/test/snapshots/api/AuthenticateApi.test.js.snap index 00bb6fa32..3ffd8a710 100644 --- a/src/test/snapshots/api/AuthenticateApi.test.js.snap +++ b/src/test/snapshots/api/AuthenticateApi.test.js.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`AuthenticateApi authenticateIdm() 1: On-prem IDM authentication 1`] = ` +{ + "_id": "login", + "authenticationId": "openidm-admin", + "authorization": { + "authLogin": true, + "authenticationId": "openidm-admin", + "component": "internal/user", + "id": "openidm-admin", + "ipAddress": "127.0.0.1", + "moduleId": "STATIC_USER", + "roles": [ + "internal/role/openidm-admin", + "internal/role/openidm-authorized", + ], + "userRolesProperty": "authzRoles", + }, +} +`; + exports[`AuthenticateApi step() 1: Single step login journey 'PasswordGrant' 1`] = ` { "realm": "/alpha",