From 9414774e89c5c21b0c2178652ded72033065e8d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Fri, 3 Feb 2023 07:54:56 +0100 Subject: [PATCH] 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 --- ...rowser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip | Bin 0 -> 19180 bytes .../AuthenticatorApiOperations.ts | 1 - .../Authenticator/AuthenticatorApiService.ts | 35 ++----- .../AuthenticatorApiServiceInterface.ts | 9 +- ...catorAuthenticationOptionsRequestParams.ts | 3 + ...atorAuthenticationResponseRequestParams.ts | 5 - ...icatorRegistrationResponseRequestParams.ts | 2 +- packages/api/src/Domain/Request/index.ts | 2 +- ...enticatorAuthenticationResponseResponse.ts | 10 -- ...catorAuthenticationResponseResponseBody.ts | 3 - packages/api/src/Domain/Response/index.ts | 2 - .../Authenticator/AuthenticatorServer.ts | 17 +--- .../AuthenticatorServerInterface.ts | 10 +- .../src/Domain/Server/Authenticator/Paths.ts | 1 - .../AuthenticatorClientInterface.ts | 5 +- .../Authenticator/AuthenticatorManager.ts | 26 +---- .../Challenge/Prompt/ChallengePrompt.ts | 6 ++ .../Domain/Challenge/Prompt/PromptTitles.ts | 1 + .../Challenge/Types/ChallengeRawValue.ts | 2 +- .../Challenge/Types/ChallengeValidation.ts | 1 + .../services/src/Domain/Strings/Messages.ts | 1 + packages/snjs/lib/Application/Application.ts | 12 +-- ...thenticatorAuthenticationResponse.spec.ts} | 49 +++++---- ...GetAuthenticatorAuthenticationResponse.ts} | 29 ++---- ...tAuthenticatorAuthenticationResponseDTO.ts | 3 + .../UseCase/UseCaseContainerInterface.ts | 2 +- .../VerifyAuthenticatorDTO.ts | 3 - packages/snjs/lib/Services/Api/ApiService.ts | 5 + .../Services/Challenge/ChallengeService.ts | 2 + .../lib/Services/Session/SessionManager.ts | 75 ++++++++++---- packages/snjs/package.json | 4 +- packages/web/jest.config.js | 1 + packages/web/package.json | 3 +- .../javascripts/Application/Application.ts | 3 + .../ChallengeModal/ChallengeModal.tsx | 32 ++++-- .../ChallengeModal/ChallengePrompt.tsx | 12 +++ .../Components/ChallengeModal/InputValue.tsx | 2 +- .../Components/ChallengeModal/U2FPrompt.tsx | 62 ++++++++++++ .../Preferences/Panes/Security/Security.tsx | 5 + .../Panes/Security/U2F/U2FAddDeviceView.tsx | 94 ++++++++++++++++++ .../Panes/Security/U2F/U2FProps.ts | 7 ++ .../Security/U2F/U2FView/U2FDescription.tsx | 19 ++++ .../Security/U2F/U2FView/U2FDevicesList.tsx | 57 +++++++++++ .../Panes/Security/U2F/U2FView/U2FTitle.tsx | 19 ++++ .../Panes/Security/U2F/U2FView/U2FView.tsx | 80 +++++++++++++++ .../Panes/Security/U2F/U2FWrapper.tsx | 10 ++ packages/web/src/javascripts/FeatureTrunk.ts | 2 + yarn.lock | 8 ++ 48 files changed, 552 insertions(+), 190 deletions(-) create mode 100644 .yarn/cache/@simplewebauthn-browser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip create mode 100644 packages/api/src/Domain/Request/Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams.ts delete mode 100644 packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams.ts delete mode 100644 packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponse.ts delete mode 100644 packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody.ts rename packages/snjs/lib/Domain/UseCase/{VerifyAuthenticator/VerifyAuthenticator.spec.ts => GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts} (55%) rename packages/snjs/lib/Domain/UseCase/{VerifyAuthenticator/VerifyAuthenticator.ts => GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts} (57%) create mode 100644 packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO.ts delete mode 100644 packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticatorDTO.ts create mode 100644 packages/web/src/javascripts/Components/ChallengeModal/U2FPrompt.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FAddDeviceView.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FProps.ts create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDescription.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDevicesList.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FTitle.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FView.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FWrapper.tsx diff --git a/.yarn/cache/@simplewebauthn-browser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip b/.yarn/cache/@simplewebauthn-browser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip new file mode 100644 index 0000000000000000000000000000000000000000..34e817595ec524436ae380d7e2f0bda735a62804 GIT binary patch literal 19180 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 literal 0 HcmV?d00001 diff --git a/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiOperations.ts b/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiOperations.ts index d4ee9fe94..976049989 100644 --- a/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiOperations.ts +++ b/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiOperations.ts @@ -4,5 +4,4 @@ export enum AuthenticatorApiOperations { GenerateRegistrationOptions, GenerateAuthenticationOptions, VerifyRegistrationResponse, - VerifyAuthenticationResponse, } diff --git a/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiService.ts b/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiService.ts index bfd184210..ff58a3e49 100644 --- a/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiService.ts +++ b/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiService.ts @@ -9,7 +9,6 @@ import { GenerateAuthenticatorRegistrationOptionsResponse, VerifyAuthenticatorRegistrationResponseResponse, GenerateAuthenticatorAuthenticationOptionsResponse, - VerifyAuthenticatorAuthenticationResponseResponse, } from '../../Response' import { AuthenticatorServerInterface } from '../../Server/Authenticator/AuthenticatorServerInterface' @@ -79,7 +78,7 @@ export class AuthenticatorApiService implements AuthenticatorApiServiceInterface async verifyRegistrationResponse( userUuid: string, name: string, - registrationCredential: Record, + attestationResponse: Record, ): Promise { if (this.operationsInProgress.get(AuthenticatorApiOperations.VerifyRegistrationResponse)) { throw new ApiCallError(ErrorMessage.GenericInProgress) @@ -91,7 +90,7 @@ export class AuthenticatorApiService implements AuthenticatorApiServiceInterface const response = await this.authenticatorServer.verifyRegistrationResponse({ userUuid, name, - registrationCredential, + attestationResponse, }) return response @@ -102,7 +101,7 @@ export class AuthenticatorApiService implements AuthenticatorApiServiceInterface } } - async generateAuthenticationOptions(): Promise { + async generateAuthenticationOptions(username: string): Promise { if (this.operationsInProgress.get(AuthenticatorApiOperations.GenerateAuthenticationOptions)) { throw new ApiCallError(ErrorMessage.GenericInProgress) } @@ -110,7 +109,9 @@ export class AuthenticatorApiService implements AuthenticatorApiServiceInterface this.operationsInProgress.set(AuthenticatorApiOperations.GenerateAuthenticationOptions, true) try { - const response = await this.authenticatorServer.generateAuthenticationOptions() + const response = await this.authenticatorServer.generateAuthenticationOptions({ + username, + }) return response } catch (error) { @@ -119,28 +120,4 @@ export class AuthenticatorApiService implements AuthenticatorApiServiceInterface this.operationsInProgress.set(AuthenticatorApiOperations.GenerateAuthenticationOptions, false) } } - - async verifyAuthenticationResponse( - userUuid: string, - authenticationCredential: Record, - ): Promise { - if (this.operationsInProgress.get(AuthenticatorApiOperations.VerifyAuthenticationResponse)) { - throw new ApiCallError(ErrorMessage.GenericInProgress) - } - - this.operationsInProgress.set(AuthenticatorApiOperations.VerifyAuthenticationResponse, true) - - try { - const response = await this.authenticatorServer.verifyAuthenticationResponse({ - authenticationCredential, - userUuid, - }) - - return response - } catch (error) { - throw new ApiCallError(ErrorMessage.GenericFail) - } finally { - this.operationsInProgress.set(AuthenticatorApiOperations.VerifyAuthenticationResponse, false) - } - } } diff --git a/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiServiceInterface.ts b/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiServiceInterface.ts index ed814b68e..1c81ef2c3 100644 --- a/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/Authenticator/AuthenticatorApiServiceInterface.ts @@ -4,7 +4,6 @@ import { GenerateAuthenticatorRegistrationOptionsResponse, VerifyAuthenticatorRegistrationResponseResponse, GenerateAuthenticatorAuthenticationOptionsResponse, - VerifyAuthenticatorAuthenticationResponseResponse, } from '../../Response' export interface AuthenticatorApiServiceInterface { @@ -14,11 +13,7 @@ export interface AuthenticatorApiServiceInterface { verifyRegistrationResponse( userUuid: string, name: string, - registrationCredential: Record, + attestationResponse: Record, ): Promise - generateAuthenticationOptions(): Promise - verifyAuthenticationResponse( - userUuid: string, - authenticationCredential: Record, - ): Promise + generateAuthenticationOptions(username: string): Promise } diff --git a/packages/api/src/Domain/Request/Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams.ts b/packages/api/src/Domain/Request/Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams.ts new file mode 100644 index 000000000..7ba7e4fb3 --- /dev/null +++ b/packages/api/src/Domain/Request/Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams.ts @@ -0,0 +1,3 @@ +export interface GenerateAuthenticatorAuthenticationOptionsRequestParams { + username: string +} diff --git a/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams.ts b/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams.ts deleted file mode 100644 index e335b6680..000000000 --- a/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface VerifyAuthenticatorAuthenticationResponseRequestParams { - userUuid: string - authenticationCredential: Record - [additionalParam: string]: unknown -} diff --git a/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams.ts b/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams.ts index 2448a8228..6bcf559c5 100644 --- a/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams.ts +++ b/packages/api/src/Domain/Request/Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams.ts @@ -1,6 +1,6 @@ export interface VerifyAuthenticatorRegistrationResponseRequestParams { userUuid: string name: string - registrationCredential: Record + attestationResponse: Record [additionalParam: string]: unknown } diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index e4744d42b..bde8a4755 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -1,7 +1,7 @@ export * from './ApiEndpointParam' export * from './Authenticator/DeleteAuthenticatorRequestParams' +export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams' export * from './Authenticator/ListAuthenticatorsRequestParams' -export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams' export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams' export * from './Recovery/RecoveryKeyParamsRequestParams' export * from './Recovery/SignInWithRecoveryCodesRequestParams' diff --git a/packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponse.ts b/packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponse.ts deleted file mode 100644 index ada9af679..000000000 --- a/packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Either } from '@standardnotes/common' - -import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' -import { HttpResponse } from '../../Http/HttpResponse' - -import { VerifyAuthenticatorAuthenticationResponseResponseBody } from './VerifyAuthenticatorAuthenticationResponseResponseBody' - -export interface VerifyAuthenticatorAuthenticationResponseResponse extends HttpResponse { - data: Either -} diff --git a/packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody.ts b/packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody.ts deleted file mode 100644 index 382d196f4..000000000 --- a/packages/api/src/Domain/Response/Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface VerifyAuthenticatorAuthenticationResponseResponseBody { - success: boolean -} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts index 2a8b6870a..862d6cab6 100644 --- a/packages/api/src/Domain/Response/index.ts +++ b/packages/api/src/Domain/Response/index.ts @@ -8,8 +8,6 @@ export * from './Authenticator/GenerateAuthenticatorRegistrationOptionsResponse' export * from './Authenticator/GenerateAuthenticatorRegistrationOptionsResponseBody' export * from './Authenticator/ListAuthenticatorsResponse' export * from './Authenticator/ListAuthenticatorsResponseBody' -export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponse' -export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody' export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponse' export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponseBody' export * from './Recovery/GenerateRecoveryCodesResponse' diff --git a/packages/api/src/Domain/Server/Authenticator/AuthenticatorServer.ts b/packages/api/src/Domain/Server/Authenticator/AuthenticatorServer.ts index a7e51acf9..7148af928 100644 --- a/packages/api/src/Domain/Server/Authenticator/AuthenticatorServer.ts +++ b/packages/api/src/Domain/Server/Authenticator/AuthenticatorServer.ts @@ -1,9 +1,9 @@ import { HttpServiceInterface } from '../../Http/HttpServiceInterface' import { ListAuthenticatorsRequestParams, + GenerateAuthenticatorAuthenticationOptionsRequestParams, DeleteAuthenticatorRequestParams, VerifyAuthenticatorRegistrationResponseRequestParams, - VerifyAuthenticatorAuthenticationResponseRequestParams, } from '../../Request' import { ListAuthenticatorsResponse, @@ -11,7 +11,6 @@ import { GenerateAuthenticatorRegistrationOptionsResponse, VerifyAuthenticatorRegistrationResponseResponse, GenerateAuthenticatorAuthenticationOptionsResponse, - VerifyAuthenticatorAuthenticationResponseResponse, } from '../../Response' import { AuthenticatorServerInterface } from './AuthenticatorServerInterface' import { Paths } from './Paths' @@ -45,17 +44,11 @@ export class AuthenticatorServer implements AuthenticatorServerInterface { return response as VerifyAuthenticatorRegistrationResponseResponse } - async generateAuthenticationOptions(): Promise { - const response = await this.httpService.get(Paths.v1.generateAuthenticationOptions) + async generateAuthenticationOptions( + params: GenerateAuthenticatorAuthenticationOptionsRequestParams, + ): Promise { + const response = await this.httpService.post(Paths.v1.generateAuthenticationOptions, params) return response as GenerateAuthenticatorAuthenticationOptionsResponse } - - async verifyAuthenticationResponse( - params: VerifyAuthenticatorAuthenticationResponseRequestParams, - ): Promise { - const response = await this.httpService.post(Paths.v1.verifyAuthenticationResponse, params) - - return response as VerifyAuthenticatorAuthenticationResponseResponse - } } diff --git a/packages/api/src/Domain/Server/Authenticator/AuthenticatorServerInterface.ts b/packages/api/src/Domain/Server/Authenticator/AuthenticatorServerInterface.ts index a1a8ed418..b5c00cb7a 100644 --- a/packages/api/src/Domain/Server/Authenticator/AuthenticatorServerInterface.ts +++ b/packages/api/src/Domain/Server/Authenticator/AuthenticatorServerInterface.ts @@ -2,7 +2,7 @@ import { ListAuthenticatorsRequestParams, DeleteAuthenticatorRequestParams, VerifyAuthenticatorRegistrationResponseRequestParams, - VerifyAuthenticatorAuthenticationResponseRequestParams, + GenerateAuthenticatorAuthenticationOptionsRequestParams, } from '../../Request' import { ListAuthenticatorsResponse, @@ -10,7 +10,6 @@ import { GenerateAuthenticatorRegistrationOptionsResponse, VerifyAuthenticatorRegistrationResponseResponse, GenerateAuthenticatorAuthenticationOptionsResponse, - VerifyAuthenticatorAuthenticationResponseResponse, } from '../../Response' export interface AuthenticatorServerInterface { @@ -20,8 +19,7 @@ export interface AuthenticatorServerInterface { verifyRegistrationResponse( params: VerifyAuthenticatorRegistrationResponseRequestParams, ): Promise - generateAuthenticationOptions(): Promise - verifyAuthenticationResponse( - params: VerifyAuthenticatorAuthenticationResponseRequestParams, - ): Promise + generateAuthenticationOptions( + params: GenerateAuthenticatorAuthenticationOptionsRequestParams, + ): Promise } diff --git a/packages/api/src/Domain/Server/Authenticator/Paths.ts b/packages/api/src/Domain/Server/Authenticator/Paths.ts index 9ec186b99..aeaa3956d 100644 --- a/packages/api/src/Domain/Server/Authenticator/Paths.ts +++ b/packages/api/src/Domain/Server/Authenticator/Paths.ts @@ -4,7 +4,6 @@ const AuthenticatorPaths = { generateRegistrationOptions: '/v1/authenticators/generate-registration-options', verifyRegistrationResponse: '/v1/authenticators/verify-registration', generateAuthenticationOptions: '/v1/authenticators/generate-authentication-options', - verifyAuthenticationResponse: '/v1/authenticators/verify-authentication', } export const Paths = { diff --git a/packages/services/src/Domain/Authenticator/AuthenticatorClientInterface.ts b/packages/services/src/Domain/Authenticator/AuthenticatorClientInterface.ts index aa7d9795d..08ede8f6e 100644 --- a/packages/services/src/Domain/Authenticator/AuthenticatorClientInterface.ts +++ b/packages/services/src/Domain/Authenticator/AuthenticatorClientInterface.ts @@ -1,4 +1,4 @@ -import { Uuid } from '@standardnotes/domain-core' +import { Username, Uuid } from '@standardnotes/domain-core' export interface AuthenticatorClientInterface { list(): Promise> @@ -9,6 +9,5 @@ export interface AuthenticatorClientInterface { name: string, registrationCredential: Record, ): Promise - generateAuthenticationOptions(): Promise | null> - verifyAuthenticationResponse(userUuid: Uuid, authenticationCredential: Record): Promise + generateAuthenticationOptions(username: Username): Promise | null> } diff --git a/packages/services/src/Domain/Authenticator/AuthenticatorManager.ts b/packages/services/src/Domain/Authenticator/AuthenticatorManager.ts index ddd1be832..100f86e4f 100644 --- a/packages/services/src/Domain/Authenticator/AuthenticatorManager.ts +++ b/packages/services/src/Domain/Authenticator/AuthenticatorManager.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ import { AuthenticatorApiServiceInterface } from '@standardnotes/api' -import { Uuid } from '@standardnotes/domain-core' +import { Username, Uuid } from '@standardnotes/domain-core' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { AbstractService } from '../Service/AbstractService' @@ -79,9 +79,9 @@ export class AuthenticatorManager extends AbstractService implements Authenticat } } - async generateAuthenticationOptions(): Promise | null> { + async generateAuthenticationOptions(username: Username): Promise | null> { try { - const result = await this.authenticatorApiService.generateAuthenticationOptions() + const result = await this.authenticatorApiService.generateAuthenticationOptions(username.value) if (result.data.error) { return null @@ -92,24 +92,4 @@ export class AuthenticatorManager extends AbstractService implements Authenticat return null } } - - async verifyAuthenticationResponse( - userUuid: Uuid, - authenticationCredential: Record, - ): Promise { - try { - const result = await this.authenticatorApiService.verifyAuthenticationResponse( - userUuid.value, - authenticationCredential, - ) - - if (result.data.error) { - return false - } - - return result.data.success - } catch (error) { - return false - } - } } diff --git a/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts b/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts index 801dbffbe..9bd95a544 100644 --- a/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts +++ b/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts @@ -20,6 +20,7 @@ export class ChallengePrompt implements ChallengePromptInterface { public readonly secureTextEntry = true, public readonly keyboardType?: ChallengeKeyboardType, public readonly initialValue?: ChallengeRawValue, + public readonly contextData?: Record, ) { switch (this.validation) { case ChallengeValidation.AccountPassword: @@ -37,6 +38,11 @@ export class ChallengePrompt implements ChallengePromptInterface { this.placeholder = placeholder ?? '' this.validates = true break + case ChallengeValidation.Authenticator: + this.title = title ?? ChallengePromptTitle.U2F + this.placeholder = placeholder ?? '' + this.validates = true + break case ChallengeValidation.ProtectionSessionDuration: this.title = title ?? ChallengePromptTitle.RememberFor this.placeholder = placeholder ?? '' diff --git a/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts b/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts index 5dd7f0cf4..7e37367ea 100644 --- a/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts +++ b/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts @@ -6,4 +6,5 @@ export const ChallengePromptTitle = { Biometrics: 'Biometrics', RememberFor: 'Remember For', Mfa: 'Two-factor Authentication Code', + U2F: 'Security Key', } diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts b/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts index b34c6606a..ca9715ab1 100644 --- a/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts +++ b/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts @@ -1,3 +1,3 @@ /* istanbul ignore file */ -export type ChallengeRawValue = number | string | boolean +export type ChallengeRawValue = number | string | boolean | Record diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts b/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts index 3f8f32b5a..fc867ca78 100644 --- a/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts +++ b/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts @@ -6,4 +6,5 @@ export enum ChallengeValidation { AccountPassword = 2, Biometric = 3, ProtectionSessionDuration = 4, + Authenticator = 5, } diff --git a/packages/services/src/Domain/Strings/Messages.ts b/packages/services/src/Domain/Strings/Messages.ts index 5b68848fc..abaed42f7 100644 --- a/packages/services/src/Domain/Strings/Messages.ts +++ b/packages/services/src/Domain/Strings/Messages.ts @@ -121,6 +121,7 @@ export const SessionStrings = { }, SessionRestored: 'Your session has been successfully restored.', EnterMfa: 'Please enter your two-factor authentication code.', + InputU2FDevice: 'Please authenticate with your U2F device.', MfaInputPlaceholder: 'Two-factor authentication code', EmailInputPlaceholder: 'Email', PasswordInputPlaceholder: 'Password', diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 77e555106..78ed3318a 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -105,11 +105,11 @@ import { GetRecoveryCodes } from '@Lib/Domain/UseCase/GetRecoveryCodes/GetRecove import { AddAuthenticator } from '@Lib/Domain/UseCase/AddAuthenticator/AddAuthenticator' import { ListAuthenticators } from '@Lib/Domain/UseCase/ListAuthenticators/ListAuthenticators' import { DeleteAuthenticator } from '@Lib/Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator' -import { VerifyAuthenticator } from '@Lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator' import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions' import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision' import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision' import { RevisionMetadata } from '@Lib/Domain/Revision/RevisionMetadata' +import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse' /** How often to automatically sync, in milliseconds */ const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 @@ -193,7 +193,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private declare _addAuthenticator: AddAuthenticator private declare _listAuthenticators: ListAuthenticators private declare _deleteAuthenticator: DeleteAuthenticator - private declare _verifyAuthenticator: VerifyAuthenticator + private declare _getAuthenticatorAuthenticationResponse: GetAuthenticatorAuthenticationResponse private declare _listRevisions: ListRevisions private declare _getRevision: GetRevision private declare _deleteRevision: DeleteRevision @@ -299,8 +299,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this._deleteAuthenticator } - get verifyAuthenticator(): UseCaseInterface { - return this._verifyAuthenticator + get getAuthenticatorAuthenticationResponse(): UseCaseInterface> { + return this._getAuthenticatorAuthenticationResponse } get listRevisions(): UseCaseInterface> { @@ -1272,7 +1272,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli ;(this._addAuthenticator as unknown) = undefined ;(this._listAuthenticators as unknown) = undefined ;(this._deleteAuthenticator as unknown) = undefined - ;(this._verifyAuthenticator as unknown) = undefined + ;(this._getAuthenticatorAuthenticationResponse as unknown) = undefined ;(this._listRevisions as unknown) = undefined ;(this._getRevision as unknown) = undefined ;(this._deleteRevision as unknown) = undefined @@ -1849,7 +1849,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this._deleteAuthenticator = new DeleteAuthenticator(this.authenticatorManager) - this._verifyAuthenticator = new VerifyAuthenticator( + this._getAuthenticatorAuthenticationResponse = new GetAuthenticatorAuthenticationResponse( this.authenticatorManager, this.options.u2fAuthenticatorVerificationPromptFunction, ) diff --git a/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator.spec.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts similarity index 55% rename from packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator.spec.ts rename to packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts index cf71cd339..882e7a9b9 100644 --- a/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator.spec.ts +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.spec.ts @@ -1,27 +1,37 @@ import { AuthenticatorClientInterface } from '@standardnotes/services' -import { VerifyAuthenticator } from './VerifyAuthenticator' +import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthenticationResponse' -describe('VerifyAuthenticator', () => { +describe('GetAuthenticatorAuthenticationResponse', () => { let authenticatorClient: AuthenticatorClientInterface let authenticatorVerificationPromptFunction: ( authenticationOptions: Record, ) => Promise> - const createUseCase = () => new VerifyAuthenticator(authenticatorClient, authenticatorVerificationPromptFunction) + const createUseCase = () => new GetAuthenticatorAuthenticationResponse(authenticatorClient, authenticatorVerificationPromptFunction) beforeEach(() => { authenticatorClient = {} as jest.Mocked authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue({ foo: 'bar' }) - authenticatorClient.verifyAuthenticationResponse = jest.fn().mockResolvedValue(true) authenticatorVerificationPromptFunction = jest.fn() }) + it('should return an error if username is not provided', async () => { + const result = await createUseCase().execute({ + username: '', + }) + + expect(result.isFailed()).toBe(true) + expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty') + }) + it('should return an error if authenticator client fails to generate authentication options', async () => { authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue(null) - const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + const result = await createUseCase().execute({ + username: 'test@test.te', + }) expect(result.isFailed()).toBe(true) expect(result.getError()).toBe('Could not generate authenticator authentication options') @@ -30,36 +40,25 @@ describe('VerifyAuthenticator', () => { it('should return an error if authenticator verification prompt function fails', async () => { authenticatorVerificationPromptFunction = jest.fn().mockRejectedValue(new Error('error')) - const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + const result = await createUseCase().execute({ + username: 'test@test.te', + }) expect(result.isFailed()).toBe(true) expect(result.getError()).toBe('Could not generate authenticator authentication options: error') }) - it('should return an error if authenticator client fails to verify authentication response', async () => { - authenticatorClient.verifyAuthenticationResponse = jest.fn().mockResolvedValue(false) - - const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) - - expect(result.isFailed()).toBe(true) - expect(result.getError()).toBe('Could not generate authenticator authentication options') - }) - - it('should return ok if authenticator client succeeds to verify authentication response', async () => { - const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' }) + it('should return ok if authenticator client succeeds to generate authenticator response', async () => { + const result = await createUseCase().execute({ + username: 'test@test.te', + }) expect(result.isFailed()).toBe(false) }) - it('should return an error if user uuid is invalid', async () => { - const result = await createUseCase().execute({ userUuid: 'invalid' }) - - expect(result.isFailed()).toBe(true) - }) - it('should return error if authenticatorVerificationPromptFunction is not provided', async () => { - const result = await new VerifyAuthenticator(authenticatorClient).execute({ - userUuid: '00000000-0000-0000-0000-000000000000', + const result = await new GetAuthenticatorAuthenticationResponse(authenticatorClient).execute({ + username: 'test@test.te', }) expect(result.isFailed()).toBe(true) diff --git a/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts similarity index 57% rename from packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator.ts rename to packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts index e1f0e732b..fad5fe6e1 100644 --- a/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticator.ts +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse.ts @@ -1,9 +1,8 @@ import { AuthenticatorClientInterface } from '@standardnotes/services' -import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core' +import { GetAuthenticatorAuthenticationResponseDTO } from './GetAuthenticatorAuthenticationResponseDTO' -import { VerifyAuthenticatorDTO } from './VerifyAuthenticatorDTO' - -export class VerifyAuthenticator implements UseCaseInterface { +export class GetAuthenticatorAuthenticationResponse implements UseCaseInterface> { constructor( private authenticatorClient: AuthenticatorClientInterface, private authenticatorVerificationPromptFunction?: ( @@ -11,20 +10,20 @@ export class VerifyAuthenticator implements UseCaseInterface { ) => Promise>, ) {} - async execute(dto: VerifyAuthenticatorDTO): Promise> { + async execute(dto: GetAuthenticatorAuthenticationResponseDTO): Promise>> { if (!this.authenticatorVerificationPromptFunction) { return Result.fail( 'Could not generate authenticator authentication options: No authenticator verification prompt function provided', ) } - const userUuidOrError = Uuid.create(dto.userUuid) - if (userUuidOrError.isFailed()) { - return Result.fail(`Could not generate authenticator authentication options: ${userUuidOrError.getError()}`) + const usernameOrError = Username.create(dto.username) + if (usernameOrError.isFailed()) { + return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`) } - const userUuid = userUuidOrError.getValue() + const username = usernameOrError.getValue() - const authenticationOptions = await this.authenticatorClient.generateAuthenticationOptions() + const authenticationOptions = await this.authenticatorClient.generateAuthenticationOptions(username) if (authenticationOptions === null) { return Result.fail('Could not generate authenticator authentication options') } @@ -36,14 +35,6 @@ export class VerifyAuthenticator implements UseCaseInterface { return Result.fail(`Could not generate authenticator authentication options: ${(error as Error).message}`) } - const verificationResponse = await this.authenticatorClient.verifyAuthenticationResponse( - userUuid, - authenticatorResponse, - ) - if (!verificationResponse) { - return Result.fail('Could not generate authenticator authentication options') - } - - return Result.ok() + return Result.ok(authenticatorResponse) } } diff --git a/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO.ts b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO.ts new file mode 100644 index 000000000..6f1882423 --- /dev/null +++ b/packages/snjs/lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO.ts @@ -0,0 +1,3 @@ +export interface GetAuthenticatorAuthenticationResponseDTO { + username: string +} diff --git a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts index 26ea0c7fe..4399a41e6 100644 --- a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts +++ b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts @@ -9,7 +9,7 @@ export interface UseCaseContainerInterface { get addAuthenticator(): UseCaseInterface get listAuthenticators(): UseCaseInterface> get deleteAuthenticator(): UseCaseInterface - get verifyAuthenticator(): UseCaseInterface + get getAuthenticatorAuthenticationResponse(): UseCaseInterface> get listRevisions(): UseCaseInterface> get getRevision(): UseCaseInterface get deleteRevision(): UseCaseInterface diff --git a/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticatorDTO.ts b/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticatorDTO.ts deleted file mode 100644 index 3ea9e5485..000000000 --- a/packages/snjs/lib/Domain/UseCase/VerifyAuthenticator/VerifyAuthenticatorDTO.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface VerifyAuthenticatorDTO { - userUuid: string -} diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index bf22149cb..cd4767d7e 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -232,6 +232,7 @@ export class SNApiService email: string mfaKeyPath?: string mfaCode?: string + authenticatorResponse?: Record }): Promise { const codeVerifier = this.crypto.generateRandomKey(256) this.inMemoryStore.setValue(StorageKey.CodeVerifier, codeVerifier) @@ -247,6 +248,10 @@ export class SNApiService params[dto.mfaKeyPath] = dto.mfaCode } + if (dto.authenticatorResponse) { + params.authenticator_response = dto.authenticatorResponse + } + return this.request({ verb: HttpVerb.Post, url: joinPaths(this.host, Paths.v2.keyParams), diff --git a/packages/snjs/lib/Services/Challenge/ChallengeService.ts b/packages/snjs/lib/Services/Challenge/ChallengeService.ts index 567012bef..d22103d8c 100644 --- a/packages/snjs/lib/Services/Challenge/ChallengeService.ts +++ b/packages/snjs/lib/Services/Challenge/ChallengeService.ts @@ -93,6 +93,8 @@ export class ChallengeService extends AbstractService implements ChallengeServic return this.protocolService.validateAccountPassword(value.value as string) case ChallengeValidation.Biometric: return { valid: value.value === true } + case ChallengeValidation.Authenticator: + return { valid: 'id' in (value.value as Record) } case ChallengeValidation.ProtectionSessionDuration: return { valid: isValidProtectionSessionLength(value.value) } default: diff --git a/packages/snjs/lib/Services/Session/SessionManager.ts b/packages/snjs/lib/Services/Session/SessionManager.ts index e5454acb5..c8f906c9e 100644 --- a/packages/snjs/lib/Services/Session/SessionManager.ts +++ b/packages/snjs/lib/Services/Session/SessionManager.ts @@ -48,6 +48,7 @@ import { ChallengeService } from '../Challenge' import { ApiCallError, ErrorMessage, + ErrorTag, HttpErrorResponseBody, HttpServiceInterface, UserApiServiceInterface, @@ -284,6 +285,35 @@ export class SNSessionManager return (response as Responses.GetAvailableSubscriptionsResponse).data! } + private async promptForU2FVerification(username: string): Promise | undefined> { + const challenge = new Challenge( + [ + new ChallengePrompt( + ChallengeValidation.Authenticator, + ChallengePromptTitle.U2F, + undefined, + false, + undefined, + undefined, + { + username, + }, + ), + ], + ChallengeReason.Custom, + true, + SessionStrings.InputU2FDevice, + ) + + const response = await this.challengeService.promptForChallengeResponse(challenge) + + if (!response) { + return undefined + } + + return response.values[0].value as Record + } + private async promptForMfaValue(): Promise { const challenge = new Challenge( [ @@ -344,31 +374,28 @@ export class SNSessionManager return registerResponse.data } - private async retrieveKeyParams( - email: string, - mfaKeyPath?: string, - mfaCode?: string, - ): Promise<{ + private async retrieveKeyParams(dto: { + email: string + mfaKeyPath?: string + mfaCode?: string + authenticatorResponse?: Record + }): Promise<{ keyParams?: SNRootKeyParams response: Responses.KeyParamsResponse | Responses.HttpResponse mfaKeyPath?: string mfaCode?: string }> { - const response = await this.apiService.getAccountKeyParams({ - email, - mfaKeyPath, - mfaCode, - }) + const response = await this.apiService.getAccountKeyParams(dto) if (response.error || isNullOrUndefined(response.data)) { - if (mfaCode) { + if (dto.mfaCode) { await this.alertService.alert(SignInStrings.IncorrectMfa) } - if (response.error?.payload?.mfa_key) { - /** Prompt for MFA code and try again */ - const inputtedCode = await this.promptForMfaValue() - if (!inputtedCode) { - /** User dismissed window without input */ + + if ([ErrorTag.U2FRequired, ErrorTag.MfaRequired].includes(response.error?.tag as ErrorTag)) { + const isU2FRequired = response.error?.tag === ErrorTag.U2FRequired + const result = isU2FRequired ? await this.promptForU2FVerification(dto.email) : await this.promptForMfaValue() + if (!result) { return { response: this.apiService.createErrorResponse( SignInStrings.SignInCanceledMissingMfa, @@ -376,19 +403,25 @@ export class SNSessionManager ), } } - return this.retrieveKeyParams(email, response.error.payload.mfa_key, inputtedCode) + + return this.retrieveKeyParams({ + email: dto.email, + mfaKeyPath: isU2FRequired ? undefined : response.error?.payload?.mfa_key, + mfaCode: isU2FRequired ? undefined : (result as string), + authenticatorResponse: isU2FRequired ? (result as Record) : undefined, + }) } else { return { response } } } /** Make sure to use client value for identifier/email */ - const keyParams = KeyParamsFromApiResponse(response as Responses.KeyParamsResponse, email) + const keyParams = KeyParamsFromApiResponse(response as Responses.KeyParamsResponse, dto.email) if (!keyParams || !keyParams.version) { return { response: this.apiService.createErrorResponse(API_MESSAGE_FALLBACK_LOGIN_FAIL), } } - return { keyParams, response, mfaKeyPath, mfaCode } + return { keyParams, response, mfaKeyPath: dto.mfaKeyPath, mfaCode: dto.mfaCode } } public async signIn( @@ -425,7 +458,9 @@ export class SNSessionManager ephemeral = false, minAllowedVersion?: Common.ProtocolVersion, ): Promise { - const paramsResult = await this.retrieveKeyParams(email) + const paramsResult = await this.retrieveKeyParams({ + email, + }) if (paramsResult.response.error) { return { response: paramsResult.response, diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 45624042f..05f81305f 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -37,6 +37,7 @@ "@babel/preset-env": "*", "@standardnotes/api": "workspace:*", "@standardnotes/common": "^1.46.4", + "@standardnotes/domain-core": "^1.11.1", "@standardnotes/domain-events": "^2.106.0", "@standardnotes/encryption": "workspace:*", "@standardnotes/features": "workspace:*", @@ -84,8 +85,5 @@ "webpack": "*", "webpack-cli": "*", "webpack-merge": "^5.8.0" - }, - "dependencies": { - "@standardnotes/domain-core": "^1.11.1" } } diff --git a/packages/web/jest.config.js b/packages/web/jest.config.js index 6e7d3de93..a0f4c1e52 100644 --- a/packages/web/jest.config.js +++ b/packages/web/jest.config.js @@ -14,6 +14,7 @@ module.exports = { '\\.(css|less|scss|sass)$': 'identity-obj-proxy', '@standardnotes/toast': 'identity-obj-proxy', '@standardnotes/styles': 'identity-obj-proxy', + '@simplewebauthn/browser': 'identity-obj-proxy', }, globals: { __WEB_VERSION__: '1.0.0', diff --git a/packages/web/package.json b/packages/web/package.json index c65af281d..12c3d82e4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -116,6 +116,7 @@ "app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write" }, "dependencies": { - "@lexical/headless": "^0.7.6" + "@lexical/headless": "^0.7.6", + "@simplewebauthn/browser": "^7.0.0" } } diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 692fbf18d..c87b01f51 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -25,6 +25,7 @@ import { ApplicationOptionsDefaults, } from '@standardnotes/snjs' import { makeObservable, observable } from 'mobx' +import { startAuthentication, startRegistration } from '@simplewebauthn/browser' import { PanelResizedData } from '@/Types/PanelResizedData' import { isAndroid, isDesktopApplication, isIOS } from '@/Utils' import { DesktopManager } from './Device/DesktopManager' @@ -83,6 +84,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter deviceInterface.environment === Environment.Mobile ? 250 : ApplicationOptionsDefaults.sleepBetweenBatches, allowMultipleSelection: deviceInterface.environment !== Environment.Mobile, allowNoteSelectionStatePersistence: deviceInterface.environment !== Environment.Mobile, + u2fAuthenticatorRegistrationPromptFunction: startRegistration, + u2fAuthenticatorVerificationPromptFunction: startAuthentication, }) makeObservable(this, { diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx index 8128c5199..10e245344 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -180,13 +180,23 @@ const ChallengeModal: FunctionComponent = ({ }, [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 = ({ ) 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 = ({ index={index} onValueChange={onValueChange} isInvalid={values[prompt.id].invalid} + contextData={prompt.contextData} /> ))} - + {shouldShowSubmitButton && ( + + )} {shouldShowForgotPasscode && ( + + ) +} + +export default U2FPrompt diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx index f1649f763..dafd12607 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx @@ -11,6 +11,8 @@ import ErroredItems from './ErroredItems' import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane' import BiometricsLock from '@/Components/Preferences/Panes/Security/BiometricsLock' import MultitaskingPrivacy from '@/Components/Preferences/Panes/Security/MultitaskingPrivacy' +import U2FWrapper from './U2F/U2FWrapper' +import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk' interface SecurityProps extends MfaProps { viewControllerManager: ViewControllerManager @@ -32,6 +34,9 @@ const Security: FunctionComponent = (props) => { userProvider={props.userProvider} application={props.application} /> + {featureTrunkEnabled(FeatureTrunkName.U2F) && ( + + )} {isNativeMobileWeb && } {isNativeMobileWeb && } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FAddDeviceView.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FAddDeviceView.tsx new file mode 100644 index 000000000..d5f19118e --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FAddDeviceView.tsx @@ -0,0 +1,94 @@ +import { FunctionComponent, useCallback, useState } from 'react' +import { observer } from 'mobx-react-lite' +import { UseCaseInterface } from '@standardnotes/snjs' + +import DecoratedInput from '@/Components/Input/DecoratedInput' +import { UserProvider } from '@/Components/Preferences/Providers' +import Modal from '@/Components/Modal/Modal' +import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' + +type Props = { + userProvider: UserProvider + addAuthenticator: UseCaseInterface + onDeviceAddingModalToggle: (show: boolean) => void + onDeviceAdded: () => Promise +} + +const U2FAddDeviceView: FunctionComponent = ({ + userProvider, + addAuthenticator, + onDeviceAddingModalToggle, + onDeviceAdded, +}) => { + const [deviceName, setDeviceName] = useState('') + const [errorMessage, setErrorMessage] = useState('') + + const handleDeviceNameChange = useCallback((deviceName: string) => { + setDeviceName(deviceName) + }, []) + + const handleAddDeviceClick = useCallback(async () => { + if (!deviceName) { + setErrorMessage('Device name is required') + return + } + + const user = userProvider.getUser() + if (user === undefined) { + setErrorMessage('User not found') + return + } + + const authenticatorAddedOrError = await addAuthenticator.execute({ + userUuid: user.uuid, + authenticatorName: deviceName, + }) + if (authenticatorAddedOrError.isFailed()) { + setErrorMessage(authenticatorAddedOrError.getError()) + return + } + + onDeviceAddingModalToggle(false) + await onDeviceAdded() + }, [deviceName, setErrorMessage, userProvider, addAuthenticator, onDeviceAddingModalToggle, onDeviceAdded]) + + const closeModal = () => { + onDeviceAddingModalToggle(false) + } + + const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) + + return ( + + Add Device + + ), + type: 'primary', + onClick: handleAddDeviceClick, + mobileSlot: 'right', + }, + ]} + > +
...Some Cool Device Picture Here...
+
+ +
+ {errorMessage &&
{errorMessage}
} +
+ ) +} + +export default observer(U2FAddDeviceView) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FProps.ts b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FProps.ts new file mode 100644 index 000000000..b868594f7 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FProps.ts @@ -0,0 +1,7 @@ +import { WebApplication } from '@/Application/Application' +import { UserProvider } from '@/Components/Preferences/Providers' + +export interface U2FProps { + userProvider: UserProvider + application: WebApplication +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDescription.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDescription.tsx new file mode 100644 index 000000000..c625a8942 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDescription.tsx @@ -0,0 +1,19 @@ +import { FunctionComponent } from 'react' +import { observer } from 'mobx-react-lite' + +import { Text } from '@/Components/Preferences/PreferencesComponents/Content' +import { UserProvider } from '@/Components/Preferences/Providers' + +type Props = { + userProvider: UserProvider +} + +const U2FDescription: FunctionComponent = ({ userProvider }) => { + if (userProvider.getUser() === undefined) { + return Sign in or register for an account to configure U2F. + } + + return Authenticate with a U2F hardware device. +} + +export default observer(U2FDescription) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDevicesList.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDevicesList.tsx new file mode 100644 index 000000000..22c8c45e3 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FDevicesList.tsx @@ -0,0 +1,57 @@ +import { FunctionComponent, useCallback } from 'react' +import { observer } from 'mobx-react-lite' + +import { Text } from '@/Components/Preferences/PreferencesComponents/Content' +import { WebApplication } from '@/Application/Application' +import Button from '@/Components/Button/Button' + +type Props = { + application: WebApplication + devices: Array<{ id: string; name: string }> + onDeviceDeleted: () => Promise + onError: (error: string) => void +} + +const U2FDevicesList: FunctionComponent = ({ application, devices, onError, onDeviceDeleted }) => { + const handleDeleteButtonOnClick = useCallback( + async (authenticatorId: string) => { + const deleteAuthenticatorOrError = await application.deleteAuthenticator.execute({ + authenticatorId, + }) + + if (deleteAuthenticatorOrError.isFailed()) { + onError(deleteAuthenticatorOrError.getError()) + + return + } + + await onDeviceDeleted() + }, + [application, onDeviceDeleted, onError], + ) + + return ( +
+ {devices.length > 0 && ( +
+
+ Devices: +
+ {devices.map((device) => ( +
+ {device.name} + +
+ ))} +
+ )} +
+ ) +} + +export default observer(U2FDevicesList) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FTitle.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FTitle.tsx new file mode 100644 index 000000000..53f286b80 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FTitle.tsx @@ -0,0 +1,19 @@ +import { FunctionComponent } from 'react' +import { observer } from 'mobx-react-lite' + +import { Title } from '@/Components/Preferences/PreferencesComponents/Content' +import { UserProvider } from '@/Components/Preferences/Providers' + +type Props = { + userProvider: UserProvider +} + +const U2FTitle: FunctionComponent = ({ userProvider }) => { + if (userProvider.getUser() === undefined) { + return Universal 2nd Factor authentication not available + } + + return Universal 2nd Factor authentication +} + +export default observer(U2FTitle) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FView.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FView.tsx new file mode 100644 index 000000000..b42b9ca40 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FView/U2FView.tsx @@ -0,0 +1,80 @@ +import { FunctionComponent, useCallback, useEffect, useState } from 'react' +import { observer } from 'mobx-react-lite' + +import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment' +import { WebApplication } from '@/Application/Application' +import { UserProvider } from '@/Components/Preferences/Providers' + +import U2FTitle from './U2FTitle' +import U2FDescription from './U2FDescription' +import Button from '@/Components/Button/Button' +import U2FAddDeviceView from '../U2FAddDeviceView' +import U2FDevicesList from './U2FDevicesList' + +type Props = { + application: WebApplication + userProvider: UserProvider +} + +const U2FView: FunctionComponent = ({ application, userProvider }) => { + const [showDeviceAddingModal, setShowDeviceAddingModal] = useState(false) + const [devices, setDevices] = useState>([]) + const [error, setError] = useState('') + + const handleAddDeviceClick = useCallback(() => { + setShowDeviceAddingModal(true) + }, []) + + const loadAuthenticatorDevices = useCallback(async () => { + const authenticatorListOrError = await application.listAuthenticators.execute() + if (authenticatorListOrError.isFailed()) { + setError(authenticatorListOrError.getError()) + + return + } + + setDevices(authenticatorListOrError.getValue()) + }, [setError, setDevices, application]) + + useEffect(() => { + loadAuthenticatorDevices().catch(console.error) + }, [loadAuthenticatorDevices]) + + return ( + <> + + +
+
+ + +
+ +
+
+ + {error &&
{error}
} + +
+
+ {showDeviceAddingModal && ( + + )} + + ) +} + +export default observer(U2FView) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FWrapper.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FWrapper.tsx new file mode 100644 index 000000000..0fe008892 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/U2F/U2FWrapper.tsx @@ -0,0 +1,10 @@ +import { FunctionComponent } from 'react' + +import { U2FProps } from './U2FProps' +import U2FView from './U2FView/U2FView' + +const U2FWrapper: FunctionComponent = (props) => { + return +} + +export default U2FWrapper diff --git a/packages/web/src/javascripts/FeatureTrunk.ts b/packages/web/src/javascripts/FeatureTrunk.ts index 50130941a..42b27203a 100644 --- a/packages/web/src/javascripts/FeatureTrunk.ts +++ b/packages/web/src/javascripts/FeatureTrunk.ts @@ -3,11 +3,13 @@ import { isDev } from '@/Utils' export enum FeatureTrunkName { Super, ImportTools, + U2F, } const FeatureTrunkStatus: Record = { [FeatureTrunkName.Super]: isDev && true, [FeatureTrunkName.ImportTools]: isDev && true, + [FeatureTrunkName.U2F]: isDev && true, } export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean { diff --git a/yarn.lock b/yarn.lock index d5ae53a8f..2e141b482 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4335,6 +4335,13 @@ __metadata: languageName: node linkType: hard +"@simplewebauthn/browser@npm:^7.0.0": + version: 7.0.0 + resolution: "@simplewebauthn/browser@npm:7.0.0" + checksum: eb8d7e2d923649c116275cc9bfbabfa27a180cd33dbf9d6a28c7aa9460ea79cd25204c9a7d76ed8cc24764da4a09b5939209aa30e9b295b9d54e497bb9b652a4 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.24.1": version: 0.24.46 resolution: "@sinclair/typebox@npm:0.24.46" @@ -5264,6 +5271,7 @@ __metadata: "@reach/listbox": ^0.18.0 "@reach/tooltip": ^0.18.0 "@reach/visually-hidden": ^0.18.0 + "@simplewebauthn/browser": ^7.0.0 "@standardnotes/authenticator": ^2.3.9 "@standardnotes/autobiography-theme": ^1.2.7 "@standardnotes/blocks-editor": "workspace:*"