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 16 commits into
base: joerger/fix-useMfa-error-state
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
@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?

web/packages/teleport/src/lib/useMfa.ts Outdated Show resolved Hide resolved
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
web/packages/teleport/src/services/api/api.ts Outdated Show resolved Hide resolved
@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 2 times, most recently from cb74946 to 34fead1 Compare January 3, 2025 21:09
@Joerger Joerger requested review from ryanclark and gzdunek January 3, 2025 21:09
web/packages/teleport/src/lib/useMfa.ts Outdated Show resolved Hide resolved
web/packages/teleport/src/services/api/api.ts Outdated Show resolved Hide resolved
* This is intended as a workaround for such cases, and should not be used
* for methods with access to the React scope. Use useMfa directly instead.
*/
export const MfaContextProvider = ({ children }: PropsWithChildren) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this unset the mfaContext in auth/api on unmount? (I'm not sure if that matters)

Copy link
Contributor Author

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 unmounting in React works, how would I go about this?

Copy link
Contributor

Choose a reason for hiding this comment

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

@Joerger To execute code on unmount, you can add an useEffect that will only execute once and return a cleanup function. The callback will be executed when component is mounted, and then the returned cleanup function will be executed when it's being unmounted. Example:

useEffect(
  () => {
    return () => {
      console.log('unmounted!');
    }
  },
  []
);

The cleanup function is executed by React every time when the useEffect dependencies change, just before the next effect callback is called or when component is unmounted. This is typically used to cancel network requests, making sure that no excess bandwidth is consumed, but most of all to protect out-of-order responses to force the component into an unexpected state. But here, since there are no dependencies, the cleanup function will only be executed once.

(Also note that the cleanup function is the very reason why you can't pass an async function directly as an useEffect callback, and you'd have to wrap it into a synchronous one. React would treat the returned promise object as a function and try to call it, with rather predictable effect.)

web/packages/teleport/src/lib/useMfa.ts Outdated Show resolved Hide resolved
setTimeout(() => {
if (!mfaContext)
throw new Error(
'Failed to set up MFA prompt for admin action. This is a bug.'
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this surface to the user? Should we be more helpful than "This is a bug"?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, looking at this again - this error doesn't go anywhere, as it's thrown within a setTimeout

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, for now I'll remove the timeout and just return a better error message:

Failed to set up MFA prompt for admin action. Please try refreshing the page to try again. If the issue persists, contact support as this is likely a bug.

web/packages/teleport/src/services/joinToken/joinToken.ts Outdated Show resolved Hide resolved
Comment on lines 41 to 48
const [mfaCtx, setMfaCtx] = useState<MfaContextValue>();

if (!mfaCtx) {
const mfaCtx = { getMfaChallengeResponse };
setMfaCtx(mfaCtx);
auth.setMfaContext(mfaCtx);
api.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.

Consider using an initializer function. It will save you a re-render and make the state more consistent.

Suggested change
const [mfaCtx, setMfaCtx] = useState<MfaContextValue>();
if (!mfaCtx) {
const mfaCtx = { getMfaChallengeResponse };
setMfaCtx(mfaCtx);
auth.setMfaContext(mfaCtx);
api.setMfaContext(mfaCtx);
}
const [mfaCtx, setMfaCtx] = useState<MfaContextValue>(()=> {
const mfaCtx = { getMfaChallengeResponse };
auth.setMfaContext(mfaCtx);
api.setMfaContext(mfaCtx);
return mfaCtx;
});

// Since this is a global object outside of the react scope, there is a marginal
// chance for a race condition here (the react scope should generally be initialized
// before this has a chance of being called). This conditional is not expected to
// be hit, but will catch any major issues that could arise from this solution.
Copy link
Contributor

@bl-nero bl-nero Jan 7, 2025

Choose a reason for hiding this comment

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

Hmm. This is quite a hack. The fact that auth service started to depend on an MFA context that is injected on the top level by context provider makes me think that it should be turned into a class (just like the MFA service) that is instantiated by TeleportContext. This would allow establishing a link to the MFA service/context/whatever when constructing the auth service. Will it be a big refactoring?

Copy link
Contributor Author

@Joerger Joerger Jan 7, 2025

Choose a reason for hiding this comment

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

It does sound like a big refactor to me, but I'm not really in a position with my level of typescript/React experience to judge or carry out this type of refactor. I also have some other important work to move onto. Do you think it's something we can live with today and fix tomorrow, or do we need to scrap this approach for now?

Note that without this change, we still support SSO MFA for admin actions, we just don't provide the user with the choice between SSO MFA and Webauthn when applicable. Instead we automatically open the Webauthn or SSO MFA pop up without a dialog, which may be jarring, but not the end of the world. In the long term we should find a solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Joerger I think that given above, it would be best to continue with the PR as is, and treat this part as a technical debt that we take to improve UX. Can you please create a tracking issue for this refactoring?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@avatus avatus Jan 10, 2025

Choose a reason for hiding this comment

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

I think that converting the auth and api objects into services/classes wouldn't be too garly. the biggest issue i see is they they depend on each other, so we'd want to handle that circular dependency somehow. The easiest way would be to store a reference to the teleport context inside each of these classes, then update any calls like auth.myMethod to this.ctx.authServices.myMethod for example.

then just instantiate each of the new services inside the Teleport context like all our other services. We could pass the mfaService as an argument to the authService constructor if we super care about order on instantiation but tbh, if we are passing the entire teleport context itself, it doesnt matter.

should we reference the parent Teleport Context object inside these two new services? eh, maybe maybe not. I dont see a problem with it.

then we can just update every callsite of api and auth to instead reference ctx.apiService and ctx.authService.

Thats my initial thoughts off the dome. I converted auth and api to classes (id only look at auth tbh, api is a mess still) and i would need to check the validity/add the comments back, but this is when i discovered the circular dep.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ill try the Context reference thing first and see how it works and maybe take it from there (separate PRs of course)

@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from 34fead1 to ebade41 Compare January 8, 2025 19:19
@Joerger Joerger requested review from ryanclark and bl-nero January 8, 2025 19:24
@Joerger Joerger changed the base branch from master to joerger/fix-useMfa-error-state January 8, 2025 20:27
@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from ebade41 to b5581df Compare January 8, 2025 20:30
@Joerger Joerger force-pushed the joerger/web-admin-mfa-dialog branch from 7f9a2a2 to 9db6d2b Compare January 8, 2025 20:48
Copy link
Contributor

@rosstimothy rosstimothy left a comment

Choose a reason for hiding this comment

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

Let's hold off on this and try to implement a less hacky solution from the start.

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.

6 participants