Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/api/AuthenticateApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
31 changes: 30 additions & 1 deletion src/api/AuthenticateApi.ts
Original file line number Diff line number Diff line change
@@ -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`;
Expand Down Expand Up @@ -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<any> {
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;
}
4 changes: 4 additions & 0 deletions src/api/BaseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
88 changes: 78 additions & 10 deletions src/ops/AuthenticateOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -155,12 +155,23 @@ let adminClientId = fidcClientId;
* @returns {string} cookie name
*/
async function determineCookieName(state: State): Promise<string> {
const data = await getServerInfo({ state });
let cookieName = null;
try {
const data = await getServerInfo({ state });
cookieName = data.cookieName;
} catch (e) {
if (
e.response?.status !== 401 ||

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what scenarios would we get a 401 or Access Denied?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I don't think we should get a 401 here since the endpoint doesn't require any credentials, so we can remove that. We do need to keep the try-catch though since there is no server info endpoint for IDM, and we don't want the program to fail because of it. @skootrivir Could you remove this if check?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we already know the deployment type by the time this call is being made? If so, it seems like it would be better to check the state to see if this is an IDM deployment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's the first time the connection is created, we don't know the deployment type, so this is necessary. I remember we tried to find a way to re-order things so that we try and get the cookie name after determining the deployment type, but it's not possible because we need the cookie name for determining the deployment type (at least for AM, Cloud, and ForgeOps deployments) since it is used in the requests that it makes.

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;
}

/**
Expand Down Expand Up @@ -335,6 +346,7 @@ async function determineDeploymentType(state: State): Promise<string> {
return deploymentType;

case Constants.CLASSIC_DEPLOYMENT_TYPE_KEY:
case Constants.IDM_DEPLOYMENT_TYPE_KEY:
debugMessage({
message: `AuthenticateOps.determineDeploymentType: end [type=${deploymentType}]`,
state,
Expand Down Expand Up @@ -409,10 +421,45 @@ async function determineDeploymentType(state: State): Promise<string> {
});
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,
});
}
}
}
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -553,6 +611,7 @@ async function getUserSessionToken(
otpCallbackHandler: otpCallback,
state,
});
if (!token) return null;
token.from_cache = false;
debugMessage({
message: `AuthenticateOps.getUserSessionToken: fresh`,
Expand Down Expand Up @@ -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()}`,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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())
Expand Down
Loading