Skip to content
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
6 changes: 3 additions & 3 deletions src/apps/mfa-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ export {
generateMfaAuthenticatorAppQrCodeUrl,
DEFAULT_MFA_SETTINGS,
} from './services/cognito'
export type { MfaMethod, MfaSettings } from './services/cognito'
export type { MfaMethod, MfaSettings, LoggedInMfaMethod } from './services/cognito'
export {
isMfaRequired,
mfaRequirementToSelectedMethods,
mfaSelectedMethodsToMfaRequirement,
formatMfaRequirementLabel,
formatMfaRequirementMethodLabel,
userMeetsMfaRequirement,
formatMfaSetupRequiredMessage,
formatMfaMethodNotAcceptedMessage,
formatMfaMethodRequirementMessage,
isMfaMethodUsedForLogin,
} from '../utils/mfa-requirement'
export type { MfaRequirementMethod } from '../utils/mfa-requirement'

Expand Down
33 changes: 33 additions & 0 deletions src/apps/services/AWSCognitoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ import {
VerifySoftwareTokenCommand,
VerifyUserAttributeCommand,
} from '@aws-sdk/client-cognito-identity-provider'
import { jwtDecode } from 'jwt-decode'
import Sentry from '../Sentry'
import { OneBlinkAppsError } from '..'

export type MfaMethod = 'authenticator' | 'sms'

export type LoggedInMfaMethod =
| 'SOFTWARE_TOKEN_MFA'
| 'SMS_MFA'
| 'NO_MFA_ENABLED'

export type MfaSettings = {
loggedInWithMfaMethod: LoggedInMfaMethod
authenticator: {
enabled: boolean
preferred: boolean
Expand All @@ -34,6 +41,7 @@ export type MfaSettings = {
}

export const DEFAULT_MFA_SETTINGS: MfaSettings = {
loggedInWithMfaMethod: 'NO_MFA_ENABLED',
authenticator: { enabled: false, preferred: false },
sms: {
enabled: false,
Expand All @@ -43,6 +51,28 @@ export const DEFAULT_MFA_SETTINGS: MfaSettings = {
},
}

export function resolveLoggedInMfaMethod(
idToken: string | undefined,
): LoggedInMfaMethod {
if (idToken) {
try {
const payload = jwtDecode<{ mfa_method?: unknown }>(idToken)
switch (payload.mfa_method) {
case 'SOFTWARE_TOKEN_MFA':
case 'SMS_MFA':
return payload.mfa_method
}
} catch (error) {
console.warn(
'Error while decoding id token for logged in MFA method',
error,
)
Sentry.captureException(error)
}
}
return 'NO_MFA_ENABLED'
}

export function resolveMfaPreferredFlags({
authenticatorEnabled,
smsEnabled,
Expand Down Expand Up @@ -630,7 +660,10 @@ export default class AWSCognitoClient {
preferredMfaSetting,
})

const loggedInWithMfaMethod = resolveLoggedInMfaMethod(this._getIdToken())

return {
loggedInWithMfaMethod,
authenticator: {
enabled: authenticatorEnabled,
preferred: authenticatorPreferred,
Expand Down
3 changes: 2 additions & 1 deletion src/apps/services/cognito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { jwtDecode } from 'jwt-decode'
import AWSCognitoClient, {
DEFAULT_MFA_SETTINGS,
LoginAttemptResponse,
LoggedInMfaMethod,
MfaMethod,
MfaSettings,
} from './AWSCognitoClient'
Expand Down Expand Up @@ -572,4 +573,4 @@ export {
generateMfaAuthenticatorAppQrCodeUrl,
DEFAULT_MFA_SETTINGS,
}
export type { LoginAttemptResponse, MfaMethod, MfaSettings }
export type { LoginAttemptResponse, LoggedInMfaMethod, MfaMethod, MfaSettings }
35 changes: 24 additions & 11 deletions src/components/mfa/MfaMethodRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type Props = {
isEnabled: boolean
isPreferred: boolean
isUsedForLogin: boolean
isSettingUp: boolean
isSettingPreferredMfaMethod: boolean
isSetupDisabled: boolean
Expand All @@ -20,6 +21,7 @@ type Props = {
description: string
detail?: string
mfaRequirementMessage?: string
mfaRequirementMessageIsWarning?: boolean
cypressPrefix: string
extraButtons?: React.ReactNode
onSetup: () => void
Expand All @@ -30,6 +32,7 @@ type Props = {
function MfaMethodRow({
isEnabled,
isPreferred,
isUsedForLogin,
isSettingUp,
isSettingPreferredMfaMethod,
isSetupDisabled,
Expand All @@ -38,6 +41,7 @@ function MfaMethodRow({
description,
detail,
mfaRequirementMessage,
mfaRequirementMessageIsWarning,
cypressPrefix,
extraButtons,
onSetup,
Expand All @@ -56,7 +60,7 @@ function MfaMethodRow({
<Chip
size="small"
label="Enabled"
color="info"
color="default"
sx={{ ml: 1 }}
data-cypress={`${cypressPrefix}-status-chip`}
/>
Expand All @@ -65,11 +69,20 @@ function MfaMethodRow({
<Chip
size="small"
label="Preferred"
color="success"
color="info"
sx={{ ml: 1 }}
data-cypress={`${cypressPrefix}-preferred-chip`}
/>
)}
{isUsedForLogin && (
<Chip
size="small"
label="Used for login"
color="success"
sx={{ ml: 1 }}
data-cypress={`${cypressPrefix}-used-for-login-chip`}
/>
)}
</Box>
<Typography variant="body2" color="text.secondary">
{description}
Expand All @@ -79,15 +92,6 @@ function MfaMethodRow({
{detail}
</Typography>
)}
{!!mfaRequirementMessage && (
<Alert
severity={isEnabled && isPreferred ? 'warning' : 'info'}
sx={{ mt: 1 }}
data-cypress={`${cypressPrefix}-mfa-requirement-message`}
>
{mfaRequirementMessage}
</Alert>
)}
</Grid>
<Grid size="auto">
<Box display="flex" gap={1} flexWrap="wrap" justifyContent="flex-end">
Expand Down Expand Up @@ -140,6 +144,15 @@ function MfaMethodRow({
</Box>
</Grid>
</Grid>

{!!mfaRequirementMessage && (
<Alert
severity={mfaRequirementMessageIsWarning ? 'warning' : 'info'}
data-cypress={`${cypressPrefix}-mfa-requirement-message`}
>
{mfaRequirementMessage}
</Alert>
)}
</Box>
)
}
Expand Down
53 changes: 44 additions & 9 deletions src/components/mfa/MultiFactorAuthentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ import MfaMethodRow from './MfaMethodRow'
import MfaSuccessSnackbar from './MfaSuccessSnackbar'
import MfaErrorSnackbar from './MfaErrorSnackbar'
import MfaStatusChip from './MfaStatusChip'
import { formatMfaMethodNotAcceptedMessage } from '../../utils/mfa-requirement'
import {
formatMfaMethodRequirementMessage,
isMfaMethodUsedForLogin,
} from '../../utils/mfa-requirement'

type Props = {
mfaRequirement: MiscTypes.MfaRequirement | undefined
ssoSetupUrl?: string
accessScopeLabel?: string
}

function MfaMethodList({ mfaRequirement }: Pick<Props, 'mfaRequirement'>) {
function MfaMethodList({
mfaRequirement,
accessScopeLabel = 'app',
}: Pick<Props, 'mfaRequirement' | 'accessScopeLabel'>) {
const {
mfaSettings,
loadingError,
Expand All @@ -52,12 +59,22 @@ function MfaMethodList({ mfaRequirement }: Pick<Props, 'mfaRequirement'>) {
}, [mfaSettings])

const authenticatorMfaRequirementMessage = useMemo(() => {
return formatMfaMethodNotAcceptedMessage('authenticatorApp', mfaRequirement)
}, [mfaRequirement])
return formatMfaMethodRequirementMessage(
'authenticatorApp',
mfaRequirement,
mfaSettings,
accessScopeLabel,
)
}, [accessScopeLabel, mfaRequirement, mfaSettings])

const smsMfaRequirementMessage = useMemo(() => {
return formatMfaMethodNotAcceptedMessage('sms', mfaRequirement)
}, [mfaRequirement])
return formatMfaMethodRequirementMessage(
'sms',
mfaRequirement,
mfaSettings,
accessScopeLabel,
)
}, [accessScopeLabel, mfaRequirement, mfaSettings])

if (isLoading) {
return (
Expand All @@ -81,13 +98,21 @@ function MfaMethodList({ mfaRequirement }: Pick<Props, 'mfaRequirement'>) {
<MfaMethodRow
isEnabled={mfaSettings.authenticator.enabled}
isPreferred={mfaSettings.authenticator.preferred}
isUsedForLogin={isMfaMethodUsedForLogin(
mfaSettings,
'authenticatorApp',
)}
isSettingUp={isSettingUpMfa && settingUpMfaMethod === 'authenticator'}
isSettingPreferredMfaMethod={isSettingPreferredMfaMethod}
isSetupDisabled={!!loadingError || isSettingUpMfa}
showSetupErrorTooltip={!!loadingError}
title="Authenticator App"
description="Use an app like Google Authenticator or Microsoft Authenticator to generate 6-digit verification codes."
mfaRequirementMessage={authenticatorMfaRequirementMessage}
mfaRequirementMessageIsWarning={
!!mfaRequirement?.authenticatorApp &&
!!authenticatorMfaRequirementMessage
}
cypressPrefix="mfa-authenticator"
onSetup={() => beginMfaSetup('authenticator')}
onDisable={() => beginDisablingMfaMethod('authenticator')}
Expand All @@ -97,6 +122,7 @@ function MfaMethodList({ mfaRequirement }: Pick<Props, 'mfaRequirement'>) {
<MfaMethodRow
isEnabled={mfaSettings.sms.enabled}
isPreferred={mfaSettings.sms.preferred}
isUsedForLogin={isMfaMethodUsedForLogin(mfaSettings, 'sms')}
isSettingUp={isSettingUpMfa && settingUpMfaMethod === 'sms'}
isSettingPreferredMfaMethod={isSettingPreferredMfaMethod}
isSetupDisabled={!!loadingError || isSettingUpMfa}
Expand All @@ -105,6 +131,9 @@ function MfaMethodList({ mfaRequirement }: Pick<Props, 'mfaRequirement'>) {
description="Receive a one-time verification code via SMS each time MFA is required."
detail={phoneDetail}
mfaRequirementMessage={smsMfaRequirementMessage}
mfaRequirementMessageIsWarning={
!!mfaRequirement?.sms && !!smsMfaRequirementMessage
}
cypressPrefix="mfa-sms"
onSetup={() => beginMfaSetup('sms')}
onDisable={() => beginDisablingMfaMethod('sms')}
Expand All @@ -127,7 +156,7 @@ function MfaMethodList({ mfaRequirement }: Pick<Props, 'mfaRequirement'>) {
)
}

function MfaSetup({ ssoSetupUrl, mfaRequirement }: Props) {
function MfaSetup({ ssoSetupUrl, mfaRequirement, accessScopeLabel }: Props) {
const { isExternalIdentityProviderUser } = useMfa()

if (ssoSetupUrl) {
Expand Down Expand Up @@ -164,7 +193,10 @@ function MfaSetup({ ssoSetupUrl, mfaRequirement }: Props) {

return (
<>
<MfaMethodList mfaRequirement={mfaRequirement} />
<MfaMethodList
mfaRequirement={mfaRequirement}
accessScopeLabel={accessScopeLabel}
/>
<MfaDisableDialog />
<MfaRemovePhoneNumberDialog />
<MfaPhoneNumberDialog />
Expand Down Expand Up @@ -213,6 +245,7 @@ function MfaSetup({ ssoSetupUrl, mfaRequirement }: Props) {
export default function MultiFactorAuthentication({
mfaRequirement,
ssoSetupUrl,
accessScopeLabel = 'App',
}: Props) {
return (
<Grid size={{ xs: 'grow', lg: 8 }}>
Expand All @@ -222,7 +255,8 @@ export default function MultiFactorAuthentication({
<Grid container spacing={2} alignItems="center">
<Grid size={{ xs: 'grow' }}>
<Typography variant="h4" fontWeight="light">
Multi Factor Authentication <MfaStatusChip />
Multi Factor Authentication{' '}
<MfaStatusChip />
</Typography>
<Box marginY={1}>
<Divider />
Expand All @@ -237,6 +271,7 @@ export default function MultiFactorAuthentication({
<MfaSetup
ssoSetupUrl={ssoSetupUrl}
mfaRequirement={mfaRequirement}
accessScopeLabel={accessScopeLabel}
/>
</Grid>
</Grid>
Expand Down
Loading
Loading