Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Okta | Use X-Forwarded-For header to forward user's IP address #2882

Merged
merged 2 commits into from
Sep 9, 2024
Merged
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
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/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const loggerMiddleware = (
) => {
logger.info(`${req.method}, ${req.path}`, undefined, {
request_id: req.get('x-request-id'),
ip: req.ip,
});
next();
};
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