diff --git a/src/api/AuthenticateApi.test.ts b/src/api/AuthenticateApi.test.ts index 3f94dcf31..769bbae68 100644 --- a/src/api/AuthenticateApi.test.ts +++ b/src/api/AuthenticateApi.test.ts @@ -116,4 +116,31 @@ 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-OpenIDM-Username': state.getUsername(), + 'X-OpenIDM-Password': state.getPassword(), + }, + }; + const response1 = await AuthenticateApi.authenticateIdm({ + body: {}, + config, + state, + }); + expect(response1.authorization.authLogin).toBeTruthy(); + expect(response1).toMatchSnapshot(); + }); + }); }); diff --git a/src/api/AuthenticateApi.ts b/src/api/AuthenticateApi.ts index 997a698d6..5c84e52e0 100644 --- a/src/api/AuthenticateApi.ts +++ b/src/api/AuthenticateApi.ts @@ -1,8 +1,9 @@ import util from 'util'; import { State } from '../shared/State'; +import { debugMessage } from '../utils/Console'; 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 +74,31 @@ export async function step({ }).post(urlString, body, config); return data; } + +/** + * + * @param {any} body POST request body + * @param {any} config request config + * @returns Promise resolving to the authentication service response + */ +export async function authenticateIdm({ + body = {}, + config = {}, + state, +}: { + body?: object; + config?: object; + realm?: string; + service?: string; + state: State; +}): Promise { + debugMessage({ + message: `AuthenticateApi.authenticateIdm: function start `, + state, + }); + const urlString = `${state.getHost()}/authentication?_action=login`; + const { data } = await generateIdmApi({ + state, + }).post(urlString, body, config); + return data; +} diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index 80365cbd3..9b4ba5848 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()), diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index e6cbeb861..94b1ce3e1 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 { authenticateIdm, step } from '../api/AuthenticateApi'; import { getServerInfo, getServerVersionInfo } from '../api/ServerInfoApi'; import Constants from '../shared/Constants'; import { State } from '../shared/State'; @@ -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; } /** @@ -335,6 +346,7 @@ async function determineDeploymentType(state: State): Promise { return deploymentType; case Constants.CLASSIC_DEPLOYMENT_TYPE_KEY: + case Constants.IDM_DEPLOYMENT_TYPE_KEY: debugMessage({ message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`, state, @@ -409,10 +421,45 @@ async function determineDeploymentType(state: State): Promise { }); deploymentType = Constants.FORGEOPS_DEPLOYMENT_TYPE_KEY; } else { - verboseMessage({ - message: `Classic deployment`['brightCyan'] + ` detected.`, - state, - }); + try { + const idmresponse = await authenticateIdm({ + body: {}, + config: {}, + state, + }); + if ( + idmresponse.authorization.authLogin + ) { + verboseMessage({ + message: + `Ping Identity IDM deployment`['brightCyan'] + + ` detected.`, + state, + }); + deploymentType = Constants.IDM_DEPLOYMENT_TYPE_KEY; + } else { + 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, + }); + } + } } } } @@ -471,7 +518,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; @@ -553,6 +611,7 @@ async function getUserSessionToken( otpCallbackHandler: otpCallback, state, }); + if (!token) return null; token.from_cache = false; debugMessage({ message: `AuthenticateOps.getUserSessionToken: fresh`, @@ -940,6 +999,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()}`, @@ -1162,7 +1222,7 @@ export async function getTokens({ }); const token = await getUserSessionToken(callbackHandler, state); if (token) state.setUserSessionTokenMeta(token); - if (usingConnectionProfile && !token.from_cache) { + if (usingConnectionProfile && (!token || !token.from_cache)) { saveConnectionProfile({ host: state.getHost(), state }); } await determineDeploymentTypeAndDefaultRealmAndVersion(state); @@ -1191,6 +1251,14 @@ export async function getTokens({ 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/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index e82e26fc5..5ddff7d13 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -349,13 +349,18 @@ 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 config = await exportAmConfigEntities({ - includeReadOnly, - onlyRealm, - onlyGlobal, - state, - }); + let config = {} as ConfigEntityExportInterface; + if (isPlatformDeployment || isClassicDeployment) { + config = await exportAmConfigEntities({ + includeReadOnly, + onlyRealm, + onlyGlobal, + state, + }); + } let globalConfig = {} as FullGlobalExportInterface; if (!onlyRealm || onlyGlobal) { @@ -372,7 +377,7 @@ export async function exportFullConfiguration({ state, }, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ); // Export servers and server properties @@ -409,7 +414,7 @@ export async function exportFullConfiguration({ exportEmailTemplates, stateObj, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) )?.emailTemplate, idm: ( @@ -423,7 +428,7 @@ export async function exportFullConfiguration({ state, }, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) )?.idm, internalRole: ( @@ -431,7 +436,7 @@ export async function exportFullConfiguration({ exportInternalRoles, stateObj, errors, - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) )?.internalRole, mapping: mappings?.mapping, @@ -440,7 +445,7 @@ export async function exportFullConfiguration({ exportRealms, stateObj, errors, - includeReadOnly || isClassicDeployment + (includeReadOnly && isPlatformDeployment) || isClassicDeployment ) )?.realm, scripttype: ( @@ -448,7 +453,7 @@ export async function exportFullConfiguration({ exportScriptTypes, stateObj, errors, - includeReadOnly || isClassicDeployment + (includeReadOnly && isPlatformDeployment) || isClassicDeployment ) )?.scripttype, secret: ( @@ -469,7 +474,12 @@ export async function exportFullConfiguration({ )?.secretstore, server: serverExport, service: ( - await exportWithErrorHandling(exportServices, globalStateObj, errors) + await exportWithErrorHandling( + exportServices, + globalStateObj, + errors, + isPlatformDeployment || isClassicDeployment + ) )?.service, site: ( await exportWithErrorHandling( @@ -499,7 +509,8 @@ export async function exportFullConfiguration({ Object.keys(globalConfig.idm) .filter( (k) => - k === 'ui/themerealm' || + (k === 'ui/themerealm' && isPlatformDeployment) || + isClassicDeployment || k === 'sync' || k.startsWith('mapping/') || k.startsWith('emailTemplate/') @@ -509,7 +520,10 @@ export async function exportFullConfiguration({ } const realmConfig = {}; - if (!onlyGlobal || onlyRealm) { + if ( + (isPlatformDeployment || isClassicDeployment) && + (!onlyGlobal || onlyRealm) + ) { // Export realm configs const activeRealm = state.getRealm(); for (const realm of Object.keys(config.realm)) { @@ -724,6 +738,9 @@ 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 +877,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'IDM Config Entities', - isPlatformDeployment && !!importData.global.idm + (isPlatformDeployment || isIdmDeployment) && !!importData.global.idm ) ); response.push( @@ -873,7 +890,8 @@ export async function importFullConfiguration({ errors, indicatorId, 'Email Templates', - isPlatformDeployment && !!importData.global.emailTemplate + (isPlatformDeployment || isIdmDeployment) && + !!importData.global.emailTemplate ) ); response.push( @@ -887,7 +905,7 @@ export async function importFullConfiguration({ errors, indicatorId, 'Mappings', - isPlatformDeployment + isPlatformDeployment || isIdmDeployment ) ); response.push( @@ -901,7 +919,8 @@ export async function importFullConfiguration({ errors, indicatorId, 'Services', - !!importData.global.service + (isPlatformDeployment || isClassicDeployment) && + !!importData.global.service ) ); response.push( @@ -938,7 +957,8 @@ export async function importFullConfiguration({ errors, indicatorId, 'Internal Roles', - isPlatformDeployment && !!importData.global.internalRole + (isPlatformDeployment || isIdmDeployment) && + !!importData.global.internalRole ) ); stopProgressIndicator({ @@ -947,281 +967,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 (isPlatformDeployment || isClassicDeployment) { + // 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, - { - importData: importData.realm[realm], - state, - }, - errors, - indicatorId, - 'Themes', - isPlatformDeployment && !!importData.realm[realm].theme - ) - ); response.push( await importWithErrorHandling( - importSecretStores, + importAmConfigEntities, { - importData: importData.realm[realm], - globalConfig: false, - secretStoreId: '', + importData: importData as unknown as ConfigEntityExportInterface, 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/shared/Constants.ts b/src/shared/Constants.ts index 441406fdf..0471c5c91 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, 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..5d7b01260 --- /dev/null +++ b/src/test/mock-recordings/AuthenticateApi_3841697636/authenticateIdm_1402198135/1-On-prem-IDM-authentication_2613316278/recording.har @@ -0,0 +1,171 @@ +{ + "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-a78b6567-d98d-477a-b45e-e4b5c545bed2" + }, + { + "name": "authorization", + "value": "Bearer " + }, + { + "name": "x-openidm-username", + "value": "openidm-admin" + }, + { + "name": "x-openidm-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": 1982, + "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": "" + } + ], + "headers": [ + { + "name": "date", + "value": "Mon, 10 Nov 2025 18:43:02 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=; 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-11-10T18:43:02.111Z", + "time": 193, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 193 + } + } + ], + "pages": [], + "version": "1.2" + } +} 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", diff --git a/src/utils/SetupPollyForFrodoLib.ts b/src/utils/SetupPollyForFrodoLib.ts index 6ff0f759c..7692843cc 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;