-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
base: master
Are you sure you want to change the base?
Conversation
18064df
to
aa5c0b7
Compare
235d2e1
to
cfe07e1
Compare
bf14661
to
4c3551c
Compare
const mfaCtx = { | ||
getAdminActionMfaResponse, | ||
useMfa, | ||
}; | ||
auth.setMfaContext(mfaCtx); |
There was a problem hiding this comment.
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
const mfaCtx = { | |
getAdminActionMfaResponse, | |
useMfa, | |
}; | |
auth.setMfaContext(mfaCtx); | |
useEffect(() => { | |
const mfaCtx = { | |
getAdminActionMfaResponse, | |
useMfa, | |
}; | |
auth.setMfaContext(mfaCtx); | |
}, [getAdminActionMfaResponse, useMfa]); |
There was a problem hiding this comment.
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);
}
getAdminActionMfaResponse, | ||
useMfa, | ||
}; | ||
auth.setMfaContext(mfaCtx); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
},
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
5ee3d03
to
e103a1f
Compare
4c3551c
to
e77d074
Compare
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).
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 Also, you should be able to retry or cancel in the mfa dialog. Let me know if you still get locked with my changes. |
const respErr = resp as Error; | ||
if (respErr) throw respErr; | ||
|
||
return resp as MfaChallengeResponse; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
scope: MfaChallengeScope.ADMIN_ACTION, | ||
}); | ||
mfaResponseForRetry = await auth.getMfaChallengeResponse(challenge); | ||
mfaResponseForRetry = await auth.getAdminActionMfaResponse(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
@bl-nero take a look at this one when you get some time please |
5905972
to
2f056e0
Compare
cb74946
to
34fead1
Compare
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