feat(dev): add u2f ui for managing devices and signing in (#2182)

* feat: add u2f ui for managing devices and signing in

* refactor: change unnecessary useState to derived constant

* fix: modal refactor

* fix(web): hide u2f under feature trunk

* fix(web): jest setup

---------

Co-authored-by: Aman Harwara <amanharwara@protonmail.com>
This commit is contained in:
Karol Sójko
2023-02-03 07:54:56 +01:00
committed by GitHub
parent b4f14c668d
commit 9414774e89
48 changed files with 552 additions and 190 deletions

View File

@@ -180,13 +180,23 @@ const ChallengeModal: FunctionComponent<Props> = ({
}, [application, challenge, onDismiss])
const biometricPrompt = challenge.prompts.find((prompt) => prompt.validation === ChallengeValidation.Biometric)
const authenticatorPrompt = challenge.prompts.find(
(prompt) => prompt.validation === ChallengeValidation.Authenticator,
)
const hasOnlyBiometricPrompt = challenge.prompts.length === 1 && !!biometricPrompt
const wasBiometricInputSuccessful = biometricPrompt && !!values[biometricPrompt.id].value
const hasOnlyAuthenticatorPrompt = challenge.prompts.length === 1 && !!authenticatorPrompt
const wasBiometricInputSuccessful = !!biometricPrompt && !!values[biometricPrompt.id].value
const wasAuthenticatorInputSuccessful = !!authenticatorPrompt && !!values[authenticatorPrompt.id].value
const hasSecureTextPrompt = challenge.prompts.some((prompt) => prompt.secureTextEntry)
const shouldShowSubmitButton = !(hasOnlyBiometricPrompt || hasOnlyAuthenticatorPrompt)
useEffect(() => {
const shouldAutoSubmit = hasOnlyBiometricPrompt && wasBiometricInputSuccessful
const shouldAutoSubmit =
(hasOnlyBiometricPrompt && wasBiometricInputSuccessful) ||
(hasOnlyAuthenticatorPrompt && wasAuthenticatorInputSuccessful)
const shouldFocusSecureTextPrompt = hasSecureTextPrompt && wasBiometricInputSuccessful
if (shouldAutoSubmit) {
submit()
} else if (shouldFocusSecureTextPrompt) {
@@ -195,7 +205,14 @@ const ChallengeModal: FunctionComponent<Props> = ({
) as HTMLInputElement | null
secureTextEntry?.focus()
}
}, [wasBiometricInputSuccessful, hasOnlyBiometricPrompt, submit, hasSecureTextPrompt])
}, [
wasBiometricInputSuccessful,
hasOnlyBiometricPrompt,
submit,
hasSecureTextPrompt,
hasOnlyAuthenticatorPrompt,
wasAuthenticatorInputSuccessful,
])
useEffect(() => {
const removeListener = application.addAndroidBackHandlerEventListener(() => {
@@ -289,12 +306,15 @@ const ChallengeModal: FunctionComponent<Props> = ({
index={index}
onValueChange={onValueChange}
isInvalid={values[prompt.id].invalid}
contextData={prompt.contextData}
/>
))}
</form>
<Button primary disabled={isProcessing} className="mt-1 mb-3.5 min-w-76" onClick={submit}>
{isProcessing ? 'Generating Keys...' : 'Submit'}
</Button>
{shouldShowSubmitButton && (
<Button primary disabled={isProcessing} className="mt-1 mb-3.5 min-w-76" onClick={submit}>
{isProcessing ? 'Generating Keys...' : 'Submit'}
</Button>
)}
{shouldShowForgotPasscode && (
<Button
className="flex min-w-76 items-center justify-center"

View File

@@ -11,6 +11,7 @@ import { ChallengeModalValues } from './ChallengeModalValues'
import { WebApplication } from '@/Application/Application'
import { InputValue } from './InputValue'
import BiometricsPrompt from './BiometricsPrompt'
import U2FPrompt from './U2FPrompt'
type Props = {
application: WebApplication
@@ -19,6 +20,7 @@ type Props = {
index: number
onValueChange: (value: InputValue['value'], prompt: ChallengePrompt) => void
isInvalid: boolean
contextData?: Record<string, unknown>
}
const ChallengeModalPrompt: FunctionComponent<Props> = ({
@@ -28,9 +30,11 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
index,
onValueChange,
isInvalid,
contextData,
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const biometricsButtonRef = useRef<HTMLButtonElement>(null)
const authenticatorButtonRef = useRef<HTMLButtonElement>(null)
const activatePrompt = useCallback(async () => {
if (prompt.validation === ChallengeValidation.Biometric) {
@@ -137,6 +141,14 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
prompt={prompt}
buttonRef={biometricsButtonRef}
/>
) : prompt.validation === ChallengeValidation.Authenticator ? (
<U2FPrompt
application={application}
onValueChange={onValueChange}
prompt={prompt}
buttonRef={authenticatorButtonRef}
contextData={contextData}
/>
) : prompt.secureTextEntry ? (
<DecoratedPasswordInput
ref={inputRef}

View File

@@ -2,6 +2,6 @@ import { ChallengePrompt } from '@standardnotes/snjs'
export type InputValue = {
prompt: ChallengePrompt
value: string | number | boolean
value: string | number | boolean | Record<string, unknown>
invalid: boolean
}

View File

@@ -0,0 +1,62 @@
import { WebApplication } from '@/Application/Application'
import { ChallengePrompt } from '@standardnotes/services'
import { RefObject, useState } from 'react'
import Button from '../Button/Button'
import Icon from '../Icon/Icon'
import { InputValue } from './InputValue'
type Props = {
application: WebApplication
onValueChange: (value: InputValue['value'], prompt: ChallengePrompt) => void
prompt: ChallengePrompt
buttonRef: RefObject<HTMLButtonElement>
contextData?: Record<string, unknown>
}
const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData }: Props) => {
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
const [error, setError] = useState('')
return (
<div className="min-w-76">
{error && <div className="text-red-500">{error}</div>}
<Button
primary
fullWidth
colorStyle={authenticatorResponse ? 'success' : 'info'}
onClick={async () => {
if (!contextData || contextData.username === undefined) {
setError('No username provided')
return
}
const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({
username: contextData.username,
})
if (authenticatorResponseOrError.isFailed()) {
setError(authenticatorResponseOrError.getError())
return
}
const authenticatorResponse = authenticatorResponseOrError.getValue()
setAuthenticatorResponse(authenticatorResponse)
onValueChange(authenticatorResponse, prompt)
}}
ref={buttonRef}
>
{authenticatorResponse ? (
<span className="flex items-center justify-center gap-3">
<Icon type="check-circle" />
Obtained Device Response
</span>
) : (
'Authenticate Device'
)}
</Button>
</div>
)
}
export default U2FPrompt