Skip to content

Commit

Permalink
feat(okta): Use X-Forwarded-For header to forward users ip address …
Browse files Browse the repository at this point in the history
…to Okta
  • Loading branch information
coldlink committed Sep 6, 2024
1 parent 89daa9c commit 5af5f27
Show file tree
Hide file tree
Showing 37 changed files with 421 additions and 161 deletions.
21 changes: 13 additions & 8 deletions src/server/controllers/changePassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const oktaIdxApiPasswordHandler = async ({
stateHandle: encryptedState.stateHandle,
},
state.requestId,
req.ip,
);

// validate the introspect response to make sure we're in the correct state
Expand Down Expand Up @@ -112,14 +113,14 @@ const oktaIdxApiPasswordHandler = async ({
// the interaction code flow, eventually redirecting the user back to where they need to go.
return await setPasswordAndRedirect({
stateHandle: encryptedState.stateHandle,

body: {
passcode: password,
},
expressReq: req,
expressRes: res,
path,
request_id: state.requestId,
ip: req.ip,
});
}
} catch (error) {
Expand Down Expand Up @@ -200,22 +201,26 @@ export const setPasswordController = (
const [recoveryToken, encryptedRegistrationConsents] =
decryptedRecoveryToken;

// We exhange the Okta recovery token for a freshly minted short-lived state
// We exchange the Okta recovery token for a freshly minted short-lived state
// token, to complete this change password operation. If the recovery token
// is invalid, we will show the user the link expired page.
const { stateToken } = await validateTokenInOkta({
recoveryToken,
ip: req.ip,
});

if (stateToken) {
const { sessionToken, _embedded } = await resetPasswordInOkta({
stateToken,
newPassword: password,
});
const { sessionToken, _embedded } = await resetPasswordInOkta(
{
stateToken,
newPassword: password,
},
req.ip,
);

const { id } = _embedded?.user ?? {};
if (id) {
await validateEmailAndPasswordSetSecurely(id);
await validateEmailAndPasswordSetSecurely(id, req.ip);
} else {
logger.error(
'Failed to set validation flags in Okta as there was no id',
Expand All @@ -229,7 +234,7 @@ export const setPasswordController = (
// When a jobs user is registering, we add them to the GRS group and set their name
if (clientId === 'jobs' && path === '/welcome') {
if (id) {
await setupJobsUserInOkta(firstName, secondName, id);
await setupJobsUserInOkta(firstName, secondName, id, req.ip);
trackMetric('JobsGRSGroupAgree::Success');
} else {
logger.error(
Expand Down
2 changes: 2 additions & 0 deletions src/server/controllers/checkPasswordToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const oktaIdxApiCheckHandler = async ({
stateHandle: encryptedState.stateHandle,
},
state.requestId,
req.ip,
);

if (path === '/welcome') {
Expand Down Expand Up @@ -237,6 +238,7 @@ export const checkTokenInOkta = async (
// return an error and we will show the link expired page.
const { _embedded } = await validateTokenInOkta({
recoveryToken,
ip: req.ip,
});
const email = _embedded?.user.profile.login;

Expand Down
28 changes: 20 additions & 8 deletions src/server/controllers/sendChangePasswordEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const sendEmailInOkta = async (

try {
// get the user object to check user status
const user = await getUser(email);
const user = await getUser(email, req.ip);

if (passcodesEnabled && usePasscodesResetPassword) {
// TODO: implement passcode reset password flow
Expand All @@ -97,7 +97,7 @@ export const sendEmailInOkta = async (
case Status.ACTIVE:
// inner try-catch block to handle specific errors from sendForgotPasswordEmail
try {
const token = await forgotPassword(user.id);
const token = await forgotPassword(user.id, req.ip);
if (!token) {
throw new OktaError({
message: `Okta user reset password failed: missing reset password token`,
Expand Down Expand Up @@ -144,7 +144,7 @@ export const sendEmailInOkta = async (
// check for user does not have a password set
// (to make sure we don't override any existing password)
if (!user.credentials.password) {
await dangerouslySetPlaceholderPassword(user.id);
await dangerouslySetPlaceholderPassword(user.id, req.ip);
// now that the placeholder password has been set, the user behaves like a
// normal user (provider = OKTA) and we can send the email by calling this method again
return sendEmailInOkta(req, res, true);
Expand All @@ -163,7 +163,10 @@ export const sendEmailInOkta = async (
// this will put them into the PROVISIONED state
// we will send them a create password email
try {
const tokenResponse = await activateUser(user.id);
const tokenResponse = await activateUser({
id: user.id,
ip: req.ip,
});
if (!tokenResponse?.token.length) {
throw new OktaError({
message: `Okta user activation failed: missing activation token`,
Expand Down Expand Up @@ -203,7 +206,10 @@ export const sendEmailInOkta = async (

// 1. deactivate the user
try {
await deactivateUser(user.id);
await deactivateUser({
id: user.id,
ip: req.ip,
});
trackMetric('OktaDeactivateUser::Success');
} catch (error) {
trackMetric('OktaDeactivateUser::Failure');
Expand Down Expand Up @@ -246,7 +252,10 @@ export const sendEmailInOkta = async (
// this will keep them in the PROVISIONED state
// we will send them a create password email
try {
const tokenResponse = await reactivateUser(user.id);
const tokenResponse = await reactivateUser({
id: user.id,
ip: req.ip,
});
if (!tokenResponse?.token.length) {
throw new OktaError({
message: `Okta user reactivation failed: missing re-activation token`,
Expand Down Expand Up @@ -286,7 +295,10 @@ export const sendEmailInOkta = async (

// 1. deactivate the user
try {
await deactivateUser(user.id);
await deactivateUser({
id: user.id,
ip: req.ip,
});
trackMetric('OktaDeactivateUser::Success');
} catch (error) {
trackMetric('OktaDeactivateUser::Failure');
Expand Down Expand Up @@ -330,7 +342,7 @@ export const sendEmailInOkta = async (
// if the user is RECOVERY or PASSWORD_EXPIRED, we use the
// dangerouslyResetPassword method to put them into the RECOVERY state
// and send them a reset password email
const token = await dangerouslyResetPassword(user.id);
const token = await dangerouslyResetPassword(user.id, req.ip);
if (!token) {
throw new OktaError({
message: `Okta user reset password failed: missing reset password token`,
Expand Down
38 changes: 32 additions & 6 deletions src/server/lib/__tests__/okta/api/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,12 @@ describe('okta#activateUser', () => {
Promise.resolve({ ok: true, json } as Response),
);

await expect(activateUser(userId, true)).resolves.toEqual(undefined);
await expect(
activateUser({
id: userId,
sendEmail: true,
}),
).resolves.toEqual(undefined);
});

test('should throw an error when a user is already activated', async () => {
Expand All @@ -259,7 +264,11 @@ describe('okta#activateUser', () => {
Promise.resolve({ ok: false, status: 403, json } as Response),
);

await expect(activateUser(userId)).rejects.toThrowError(
await expect(
activateUser({
id: userId,
}),
).rejects.toThrowError(
new OktaError({
message: 'Activation failed because the user is already active',
}),
Expand All @@ -277,7 +286,12 @@ describe('okta#reactivateUser', () => {
Promise.resolve({ ok: true, json } as Response),
);

await expect(reactivateUser(userId, true)).resolves.toEqual(undefined);
await expect(
reactivateUser({
id: userId,
sendEmail: true,
}),
).resolves.toEqual(undefined);
});

test('throw a an error when a user cannot be reactivated', async () => {
Expand All @@ -295,7 +309,11 @@ describe('okta#reactivateUser', () => {
Promise.resolve({ ok: false, status: 403, json } as Response),
);

await expect(reactivateUser(userId)).rejects.toThrow(
await expect(
reactivateUser({
id: userId,
}),
).rejects.toThrow(
new OktaError({
message: "This operation is not allowed in the user's current status.",
}),
Expand All @@ -311,7 +329,11 @@ describe('okta#clearUserSessions', () => {
test('should clear user sessions', async () => {
mockedFetch.mockReturnValueOnce(Promise.resolve({ ok: true } as Response));

await expect(clearUserSessions(userId)).resolves.toEqual(undefined);
await expect(
clearUserSessions({
id: userId,
}),
).resolves.toEqual(undefined);
});

test('should throw an error when a user session cannot be cleared', async () => {
Expand All @@ -328,7 +350,11 @@ describe('okta#clearUserSessions', () => {
Promise.resolve({ ok: false, status: 404, json } as Response),
);

await expect(clearUserSessions(userId)).rejects.toThrow(
await expect(
clearUserSessions({
id: userId,
}),
).rejects.toThrow(
new OktaError({
message: 'Not found: Resource not found: <userId> (User)',
}),
Expand Down
26 changes: 19 additions & 7 deletions src/server/lib/__tests__/okta/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,27 @@ const mockedCreateOktaUser =
const mockedFetchOktaUser =
mocked<(id: string) => Promise<UserResponse>>(getUser);
const mockedActivateOktaUser =
mocked<(id: string, sendEmail: boolean) => Promise<TokenResponse | void>>(
activateUser,
);
mocked<
({
id,
sendEmail,
}: {
id: string;
sendEmail: boolean;
}) => Promise<TokenResponse | void>
>(activateUser);
const mockedReactivateOktaUser =
mocked<(id: string, sendEmail: boolean) => Promise<TokenResponse | void>>(
reactivateUser,
);
mocked<
({
id,
sendEmail,
}: {
id: string;
sendEmail: boolean;
}) => Promise<TokenResponse | void>
>(reactivateUser);
const mockedDangerouslyResetPassword = mocked<
(id: string, sendEmail: boolean) => Promise<string | void>
(id: string) => Promise<string | void>
>(dangerouslyResetPassword);
const mockedGetUserGroups =
mocked<(id: string) => Promise<Group[]>>(getUserGroups);
Expand Down
17 changes: 11 additions & 6 deletions src/server/lib/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const setupJobsUserInOkta = (
firstName: string,
lastName: string,
id: string,
ip?: string,
) => {
if (firstName === '' || lastName === '') {
throw new Error('Empty values not permitted for first or last name.');
Expand All @@ -17,11 +18,15 @@ export const setupJobsUserInOkta = (
// When `isJobsUser` is set to true, Madgex will see that the user belongs to the GRS group
// because we have made the `isJobsUser` flag the source of truth for this group membership
// when IDAPI returns the user's groups, overriding the value stored in Postgres.
return updateUser(id, {
profile: {
isJobsUser: true,
firstName,
lastName,
return updateUser(
id,
{
profile: {
isJobsUser: true,
firstName,
lastName,
},
},
});
ip,
);
};
1 change: 1 addition & 0 deletions src/server/lib/middleware/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const loginMiddlewareOAuth = async (
// if there is an okta session cookie, check if it is valid, if not `getSession` will throw an error
await getCurrentSession({
idx: oktaIdentityEngineSessionCookieId,
ip: req.ip,
});
} catch (error) {
trackMetric('LoginMiddlewareOAuth::NoOktaSession');
Expand Down
1 change: 1 addition & 0 deletions src/server/lib/middleware/redirectIfLoggedIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const redirectIfLoggedIn = async (
// (this throws if the session is invalid)
const session = await getCurrentSession({
idx: oktaIdentityEngineSessionCookieId,
ip: req.ip,
});

// pull the user email from the session, which we need to display
Expand Down
2 changes: 1 addition & 1 deletion src/server/lib/okta/api/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const getApp = async (id: string): Promise<AppResponse> => {
const path = buildUrl(`/api/v1/apps/:id`, { id });

const app = await fetch(joinUrl(okta.orgUrl, path), {
headers: { ...defaultHeaders, ...authorizationHeader() },
headers: { ...defaultHeaders(), ...authorizationHeader() },
}).then(handleAppResponse);

AppCache.set(id, app);
Expand Down
Loading

0 comments on commit 5af5f27

Please sign in to comment.