-
Couldn't load subscription status.
- Fork 218
fix(settings): redirect user on AAL mismatch in mfa guard #19599
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,8 @@ import * as Sentry from '@sentry/react'; | |
| import { MfaErrorBoundary } from './error-boundary'; | ||
| import { getLocalizedErrorMessage } from '../../../lib/error-utils'; | ||
| import GleanMetrics from '../../../lib/glean'; | ||
| import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery'; | ||
| import LoadingSpinner from 'fxa-react/components/LoadingSpinner'; | ||
|
|
||
| /** | ||
| * This is a guard component designed to wrap around components that perform | ||
|
|
@@ -58,9 +60,12 @@ export const MfaGuard = ({ | |
| const [resendCodeLoading, setResendCodeLoading] = useState(false); | ||
| const [showResendSuccessBanner, setShowResendSuccessBanner] = useState(false); | ||
|
|
||
| const [modalLoading, setModalLoading] = useState(true); | ||
|
|
||
| const resetStates = useCallback(() => { | ||
| setLocalizedErrorBannerMessage(undefined); | ||
| setShowResendSuccessBanner(false); | ||
| setModalLoading(true); | ||
| }, []); | ||
|
|
||
| // Reactive state: if the store state changes, a re-render is triggered | ||
|
|
@@ -71,6 +76,7 @@ export const MfaGuard = ({ | |
| const account = useAccount(); | ||
| const authClient = useAuthClient(); | ||
| const navigate = useNavigate(); | ||
| const navigateWithQuery = useNavigateWithQuery(); | ||
| const sessionToken = getSessionToken(); | ||
|
|
||
| const ftlMsgResolver = useFtlMsgResolver(); | ||
|
|
@@ -105,7 +111,25 @@ export const MfaGuard = ({ | |
| // Modal Setup | ||
| useEffect(() => { | ||
| (async () => { | ||
| // To avoid requesting multiple OTPs on mount | ||
| // Ensure the session meets the minimum AAL required | ||
| const { | ||
| details: { sessionVerificationMeetsMinimumAAL }, | ||
| } = await authClient.sessionStatus(sessionToken); | ||
| if (!sessionVerificationMeetsMinimumAAL) { | ||
| console.warn('2FA must be entered to access /settings!'); | ||
| navigateWithQuery('/signin_totp_code', { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When testing locally, I see a flash of the child component before navigation - we should render a LoadingSpinner until we've resolved AAL/JWT checks |
||
| state: { | ||
| email: account.email, | ||
| sessionToken: sessionToken, | ||
| uid: account.uid, | ||
| verified: false, | ||
| isSessionAALUpgrade: true, | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| setModalLoading(false); | ||
|
|
||
| if (JwtTokenCache.hasToken(sessionToken, requiredScope)) { | ||
| return; | ||
| } | ||
|
|
@@ -122,10 +146,6 @@ export const MfaGuard = ({ | |
| await authClient.mfaRequestOtp(sessionToken, requiredScope); | ||
| } catch (err) { | ||
| MfaOtpRequestCache.remove(sessionToken, requiredScope); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for removing the call to the error boundary? Assuming there's a good reason, we probably want to also clear the MfaOtpRequestCache request cache too so it's consistent with the error-boundaries logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't make sense to call the error boundary here, since the MFA error boundary handles invalid jwt and does not handle other authorization errors. (In this case it is not even possible for MfaErrorBoundary to differentiate invalid jwt and AAL mismatch, since they use the same errno |
||
| if (err.code === 401) { | ||
| handleError(err); | ||
| return; | ||
| } | ||
| if (err.code === 429) { | ||
| setShowResendSuccessBanner(false); | ||
| setLocalizedErrorBannerMessage( | ||
|
|
@@ -150,6 +170,9 @@ export const MfaGuard = ({ | |
| onDismiss, | ||
| config.mfa.otp.expiresInMinutes, | ||
| debounce, | ||
| navigateWithQuery, | ||
| account.email, | ||
| account.uid, | ||
| ]); | ||
|
|
||
| const onSubmitOtp = async (code: string) => { | ||
|
|
@@ -191,11 +214,6 @@ export const MfaGuard = ({ | |
| } catch (err) { | ||
| MfaOtpRequestCache.remove(sessionToken, requiredScope); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment above about removing call to error boundary. |
||
| setShowResendSuccessBanner(false); | ||
| if (err.code === 401) { | ||
| handleError(err); | ||
| return; | ||
| } | ||
| setShowResendSuccessBanner(false); | ||
| setLocalizedErrorBannerMessage( | ||
| getLocalizedErrorMessage(ftlMsgResolver, err) | ||
| ); | ||
|
|
@@ -207,24 +225,25 @@ export const MfaGuard = ({ | |
| const email = account.email; | ||
| const expirationTime = config.mfa.otp.expiresInMinutes; | ||
|
|
||
| const getModal = () => ( | ||
| <Modal | ||
| {...{ | ||
| email, | ||
| expirationTime, | ||
| onSubmit: onSubmitOtp, | ||
| onDismiss, | ||
| handleResendCode, | ||
| clearErrorMessage: () => setLocalizedErrorBannerMessage(undefined), | ||
| resendCodeLoading, | ||
| showResendSuccessBanner, | ||
| localizedErrorBannerMessage, | ||
| reason, | ||
| }} | ||
| > | ||
| <p>Re-verify Account!</p> | ||
| </Modal> | ||
| ); | ||
| const getModal = () => | ||
| modalLoading ? ( | ||
| <LoadingSpinner fullScreen /> | ||
| ) : ( | ||
| <Modal | ||
| {...{ | ||
| email, | ||
| expirationTime, | ||
| onSubmit: onSubmitOtp, | ||
| onDismiss, | ||
| handleResendCode, | ||
| clearErrorMessage: () => setLocalizedErrorBannerMessage(undefined), | ||
| resendCodeLoading, | ||
| showResendSuccessBanner, | ||
| localizedErrorBannerMessage, | ||
| reason, | ||
| }} | ||
| /> | ||
| ); | ||
|
|
||
| // If we don't have a JWT, we need to open the modal to prompt for it. | ||
| // Note: I'm torn on whether we should render the child components or not. It seems | ||
|
|
||
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 feel like this logic really belogns in an error boundary. If we inline it in this component, I feel like we will inevtiably start copying this logic into any component that could potentially hit that states. That's alot of extra checks and code to copy around!
I think the better approach is making sure the error response from the server for an insuffecient AAL is consistent (it should be since this is check is now down the auth-schemes), and then respond to it at the error boundary.