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

Use Web MFA dialog for admin actions #50373

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

Joerger
Copy link
Contributor

@Joerger Joerger commented Dec 18, 2024

Changelog: Add MFA dialog in the WebUI for Admin actions instead of automatically opening up a webauthn/sso pop up.

Adds a global MFA context for prompting MFA from non-react contexts. Currently this is only used for admin actions, which prompts for MFA from basic API requests.

Depends on #49794

@Joerger Joerger changed the title Use Web MFA Dialog for Admin actui Use Web MFA dialog for admin actions Dec 18, 2024
@Joerger Joerger mentioned this pull request Dec 18, 2024
@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from 18064df to aa5c0b7 Compare December 18, 2024 19:37
@Joerger Joerger marked this pull request as ready for review December 18, 2024 20:00
@github-actions github-actions bot requested review from avatus and gzdunek December 18, 2024 20:01
@Joerger Joerger requested review from ryanclark and bl-nero December 18, 2024 20:02
@Joerger Joerger force-pushed the joerger/sso-mfa-method branch from 235d2e1 to cfe07e1 Compare December 19, 2024 02:35
@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch 3 times, most recently from bf14661 to 4c3551c Compare December 19, 2024 03:08
web/packages/teleport/src/JoinTokens/JoinTokens.tsx Outdated Show resolved Hide resolved
web/packages/teleport/src/MFAContext/MFAContext.tsx Outdated Show resolved Hide resolved
Comment on lines 33 to 37
const mfaCtx = {
getAdminActionMfaResponse,
useMfa,
};
auth.setMfaContext(mfaCtx);
Copy link
Contributor

Choose a reason for hiding this comment

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

This recreates the object and sets it on every re-render

Suggested change
const mfaCtx = {
getAdminActionMfaResponse,
useMfa,
};
auth.setMfaContext(mfaCtx);
useEffect(() => {
const mfaCtx = {
getAdminActionMfaResponse,
useMfa,
};
auth.setMfaContext(mfaCtx);
}, [getAdminActionMfaResponse, useMfa]);

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm afraid useEffect could be called too late. If we must do this (see my other comment) then I would try something like:

if (!auth.mfaContext) {
  const mfaCtx = {
        getAdminActionMfaResponse,
        useMfa,
      };
  auth.setMfaContext(mfaCtx);
}

web/packages/teleport/src/MFAContext/MFAContext.tsx Outdated Show resolved Hide resolved
web/packages/teleport/src/MFAContext/MFAContext.tsx Outdated Show resolved Hide resolved
web/packages/teleport/src/MFAContext/MFAContext.tsx Outdated Show resolved Hide resolved
getAdminActionMfaResponse,
useMfa,
};
auth.setMfaContext(mfaCtx);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, we really shouldn't do this, but I see where it came from :/
It's soooo poor that auth and api are global objects that live outside of React scope, so there is no way to pass mfaContext as a dependency to them.

The main problem now is that we rely on this side effect, we need to ensure proper coordination so that this function is executed before the callsites attempt to use the value.
Did you consider passing that mfaCtx as a parameter to all the methods that need it? Is there a lot of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did you consider passing that mfaCtx as a parameter to all the methods that need it? Is there a lot of them?

Yeah, it will be needed for every api call so I was trying to avoid that. If it's necessary I can bite the bullet and try that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's only one callsite to auth.mfaContext. What if we added a conditional there?

 async getAdminActionMfaResponse(allowReuse?: boolean) {
    if (!mfaContext) return;
    return mfaContext.getAdminActionMfaResponse(allowReuse);
  },

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it will be needed for every api call so I was trying to avoid that. If it's necessary I can bite the bullet and try that way.

Then I think we have to stay with this side effect.

There's only one callsite to auth.mfaContext. What if we added a conditional there?

async getAdminActionMfaResponse(allowReuse?: boolean) {
if (!mfaContext) return;
return mfaContext.getAdminActionMfaResponse(allowReuse);
},

I think it makes sense, but I'd throw an error if there's no mfa context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, I'll also add a 1 second timeout to try again before throwing the error.

@gzdunek
Copy link
Contributor

gzdunek commented Dec 19, 2024

Three levels of dialogs 💀
image

Maybe this one should be closed as soon as the user selects a method? I see an error in the background anyways and here I can't take any action.
image

@Joerger Joerger force-pushed the joerger/sso-mfa-method branch 2 times, most recently from 5ee3d03 to e103a1f Compare December 19, 2024 20:28
Base automatically changed from joerger/sso-mfa-method to master December 20, 2024 04:10
@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from 4c3551c to e77d074 Compare January 2, 2025 21:32
@Joerger
Copy link
Contributor Author

Joerger commented Jan 3, 2025

@gzdunek

Three levels of dialogs 💀

Yeah, it might be nice to do some type of moving flow like we do for device management. e.g. edit user -> verify your identity in the same dialog, with option to continue (mfa challenge) or back (edit user page with error telling user mfa is required).

Maybe this one should be closed as soon as the user selects a method? I see an error in the background anyways and here I can't take any action.

Thanks for pointing this out, the cancellation logic turned out to be pretty fragile and only worked well for per-session MFA with SSH sessions. I've fixed it so you should now see just one error at a time - 0149c12

Screenshot 2025-01-02 at 7 57 22 PM
Screenshot 2025-01-02 at 7 57 32 PM

Also, you should be able to retry or cancel in the mfa dialog. Let me know if you still get locked with my changes. useMfa isn't safe to be called multiple times at the same time, so it's possible you're running into the consequences of that somehow?

Comment on lines 133 to 141
const respErr = resp as Error;
if (respErr) throw respErr;

return resp as MfaChallengeResponse;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure how this works. Don't we always throw resp? err is always undefined since we never reject the promise. So it seems that we always throw MfaChallengeResponse or Error.

In case you wanted to use the assertion resp as Error to check if resp is an error, then it can't work. This assertion exists only at the compile-time. I'd come back to rejecting the promise instead of resolving it with Error.

Also, you should be able to retry or cancel in the mfa dialog. Let me know if you still get locked with my changes. useMfa isn't safe to be called multiple times at the same time, so it's possible you're running into the consequences of that somehow?

I just tested this, and as I expected, we always throw an error (and there is no way to cancel or retry in the dialog)

mfa.error.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Throwing the error inside of the useAsync method getReponse causes the error to appear in the AuthnDialog (Verify Identity) rather than the parent dialog, which is the issue I am trying to solve.

I misunderstood how the as operator works, I though it would work as a type switch, setting it to null | undefined if the type didn't match. Is the issue that the value is both of type MfaChallengeResponse | Error rather than any?

Anyways, it looks like this works instead:

if (resp instanceof Error) {
    throw resp;
}

Any issues with this?

web/packages/teleport/src/MFAContext/MFAContext.tsx Outdated Show resolved Hide resolved
web/packages/teleport/src/services/auth/auth.ts Outdated Show resolved Hide resolved
scope: MfaChallengeScope.ADMIN_ACTION,
});
mfaResponseForRetry = await auth.getMfaChallengeResponse(challenge);
mfaResponseForRetry = await auth.getAdminActionMfaResponse();
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a circular dependency here. auth depends on api and api depends on auth (I know that's not a new thing). Let me know if this doesn't make sense, but maybe it could be api responsibility to take care of MFA?
I can imagine that instead of manually calling auth.getAdminActionMfaResponse(true) in every place where we need the MFA response, we would do:

    return api
      .post(cfg.getAwsRdsDbsDeployServicesUrl(integrationName), req, null, {
        scope: 'admin-action',
        reusable: true
      })

And then api would call the mfa context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In some cases we need to reuse the same MFA response for multiple API calls, hence the reusable response:

const mfaResponse = await auth.getAdminActionMfaResponse(true);
    return ctx.userService
      .createUser(u, ExcludeUserField.Traits, mfaResponse)
      .then(result => setUsers([result, ...users]))
      .then(() =>
        ctx.userService.createResetPasswordToken(u.name, 'invite', mfaResponse)
      );

We could include an mfaContext in both auth and api instead?

@avatus
Copy link
Contributor

avatus commented Jan 3, 2025

@bl-nero take a look at this one when you get some time please

@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from 5905972 to 2f056e0 Compare January 3, 2025 21:03
@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from cb74946 to 34fead1 Compare January 3, 2025 21:09
@Joerger Joerger requested review from ryanclark and gzdunek January 3, 2025 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants