diff --git a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts index 47bf8aa8b70..d744d0a760b 100644 --- a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts @@ -86,6 +86,9 @@ export type BrowserFido2Message = { sessionId: string } & ( type: "AbortResponse"; fallbackRequested: boolean; } + | { + type: "CloseRequest"; + } ); export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { @@ -237,6 +240,13 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi await this.receive("AbortResponse"); } + async close() { + await this.send({ type: "CloseRequest", sessionId: this.sessionId }); + this.closed = true; + this.destroy$.next(); + this.destroy$.complete(); + } + private async send(msg: BrowserFido2Message): Promise { if (!this.connected$.value) { await this.connect(); @@ -276,10 +286,4 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi ); await firstValueFrom(this.connected$.pipe(filter((connected) => connected === true))); } - - private close() { - this.closed = true; - this.destroy$.next(); - this.destroy$.complete(); - } } diff --git a/apps/browser/src/webauthn/popup/fido2/fido2.component.html b/apps/browser/src/webauthn/popup/fido2/fido2.component.html index 0f7b44d35f7..10e3d7bdbce 100644 --- a/apps/browser/src/webauthn/popup/fido2/fido2.component.html +++ b/apps/browser/src/webauthn/popup/fido2/fido2.component.html @@ -1,5 +1,6 @@
+ A site is asking for authentication using the following credential:
diff --git a/apps/browser/src/webauthn/popup/fido2/fido2.component.ts b/apps/browser/src/webauthn/popup/fido2/fido2.component.ts index 665ad613f35..e98a83c6804 100644 --- a/apps/browser/src/webauthn/popup/fido2/fido2.component.ts +++ b/apps/browser/src/webauthn/popup/fido2/fido2.component.ts @@ -33,6 +33,7 @@ export class Fido2Component implements OnInit, OnDestroy { protected data$ = new BehaviorSubject(null); protected sessionId?: string; protected ciphers?: CipherView[] = []; + protected loading = false; constructor(private activatedRoute: ActivatedRoute, private cipherService: CipherService) {} @@ -92,6 +93,8 @@ export class Fido2Component implements OnInit, OnDestroy { return cipher.decrypt(); }) ); + } else if (data?.type === "CloseRequest") { + window.close(); } }), takeUntil(this.destroy$) @@ -122,7 +125,7 @@ export class Fido2Component implements OnInit, OnDestroy { }); } - window.close(); + this.loading = true; } confirm() { @@ -130,7 +133,7 @@ export class Fido2Component implements OnInit, OnDestroy { sessionId: this.sessionId, type: "ConfirmCredentialResponse", }); - window.close(); + this.loading = true; } confirmNew() { @@ -138,7 +141,7 @@ export class Fido2Component implements OnInit, OnDestroy { sessionId: this.sessionId, type: "ConfirmNewCredentialResponse", }); - window.close(); + this.loading = true; } abort(fallback: boolean) { diff --git a/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts b/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts index 81fe02755e5..2cd354556e0 100644 --- a/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/webauthn/abstractions/fido2-user-interface.service.abstraction.ts @@ -41,4 +41,5 @@ export abstract class Fido2UserInterfaceSession { existingCipherIds: string[], abortController?: AbortController ) => Promise; + close: () => void; } diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index dc969106ddc..99959b00800 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -43,114 +43,121 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr ): Promise { const userInterfaceSession = await this.userInterface.newSession(abortController); - if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported); - } + try { + if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported); + } - if (params.requireResidentKey != undefined && typeof params.requireResidentKey !== "boolean") { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); - } + if ( + params.requireResidentKey != undefined && + typeof params.requireResidentKey !== "boolean" + ) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } - if ( - params.requireUserVerification != undefined && - typeof params.requireUserVerification !== "boolean" - ) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); - } + if ( + params.requireUserVerification != undefined && + typeof params.requireUserVerification !== "boolean" + ) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } - if (params.requireUserVerification) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); - } + if (params.requireUserVerification) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); + } - const existingCipherIds = await this.findExistingCredentials( - params.excludeCredentialDescriptorList - ); - if (existingCipherIds.length > 0) { - await userInterfaceSession.informExcludedCredential(existingCipherIds, abortController); - - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); - } - - let cipher: CipherView; - let fido2Key: Fido2KeyView; - let keyPair: CryptoKeyPair; - if (params.requireResidentKey) { - const userVerification = await userInterfaceSession.confirmNewCredential( - { - credentialName: params.rpEntity.name, - userName: params.userEntity.displayName, - }, - abortController + const existingCipherIds = await this.findExistingCredentials( + params.excludeCredentialDescriptorList ); + if (existingCipherIds.length > 0) { + await userInterfaceSession.informExcludedCredential(existingCipherIds, abortController); - if (!userVerification) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); } - try { - keyPair = await createKeyPair(); + let cipher: CipherView; + let fido2Key: Fido2KeyView; + let keyPair: CryptoKeyPair; + if (params.requireResidentKey) { + const userVerification = await userInterfaceSession.confirmNewCredential( + { + credentialName: params.rpEntity.name, + userName: params.userEntity.displayName, + }, + abortController + ); - cipher = new CipherView(); - cipher.type = CipherType.Fido2Key; - cipher.name = params.rpEntity.name; - cipher.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); - const encrypted = await this.cipherService.encrypt(cipher); - await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here - cipher.id = encrypted.id; - } catch { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + if (!userVerification) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); + } + + try { + keyPair = await createKeyPair(); + + cipher = new CipherView(); + cipher.type = CipherType.Fido2Key; + cipher.name = params.rpEntity.name; + cipher.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); + const encrypted = await this.cipherService.encrypt(cipher); + await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here + cipher.id = encrypted.id; + } catch { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } + } else { + const cipherId = await userInterfaceSession.confirmNewNonDiscoverableCredential( + { + credentialName: params.rpEntity.name, + userName: params.userEntity.displayName, + }, + abortController + ); + + if (cipherId === undefined) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); + } + + try { + keyPair = await createKeyPair(); + + const encrypted = await this.cipherService.get(cipherId); + cipher = await encrypted.decrypt(); + cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); + const reencrypted = await this.cipherService.encrypt(cipher); + await this.cipherService.updateWithServer(reencrypted); + } catch { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } } - } else { - const cipherId = await userInterfaceSession.confirmNewNonDiscoverableCredential( - { - credentialName: params.rpEntity.name, - userName: params.userEntity.displayName, - }, - abortController + + const credentialId = + cipher.type === CipherType.Fido2Key ? cipher.id : cipher.login.fido2Key.nonDiscoverableId; + + const authData = await generateAuthData({ + rpId: params.rpEntity.id, + credentialId: Utils.guidToRawFormat(credentialId), + counter: fido2Key.counter, + userPresence: true, + userVerification: false, + keyPair, + }); + const attestationObject = new Uint8Array( + CBOR.encode({ + fmt: "none", + attStmt: {}, + authData, + }) ); - if (cipherId === undefined) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); - } - - try { - keyPair = await createKeyPair(); - - const encrypted = await this.cipherService.get(cipherId); - cipher = await encrypted.decrypt(); - cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); - const reencrypted = await this.cipherService.encrypt(cipher); - await this.cipherService.updateWithServer(reencrypted); - } catch { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); - } - } - - const credentialId = - cipher.type === CipherType.Fido2Key ? cipher.id : cipher.login.fido2Key.nonDiscoverableId; - - const authData = await generateAuthData({ - rpId: params.rpEntity.id, - credentialId: Utils.guidToRawFormat(credentialId), - counter: fido2Key.counter, - userPresence: true, - userVerification: false, - keyPair, - }); - const attestationObject = new Uint8Array( - CBOR.encode({ - fmt: "none", - attStmt: {}, + return { + credentialId: Utils.guidToRawFormat(credentialId), + attestationObject, authData, - }) - ); - - return { - credentialId: Utils.guidToRawFormat(credentialId), - attestationObject, - authData, - publicKeyAlgorithm: -7, - }; + publicKeyAlgorithm: -7, + }; + } finally { + userInterfaceSession.close(); + } } async getAssertion( @@ -159,85 +166,89 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr ): Promise { const userInterfaceSession = await this.userInterface.newSession(abortController); - if ( - params.requireUserVerification != undefined && - typeof params.requireUserVerification !== "boolean" - ) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); - } - - if (params.requireUserVerification) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); - } - - let cipherOptions: CipherView[]; - - // eslint-disable-next-line no-empty - if (params.allowCredentialDescriptorList?.length > 0) { - cipherOptions = await this.findNonDiscoverableCredentials( - params.allowCredentialDescriptorList, - params.rpId - ); - } else { - cipherOptions = await this.findDiscoverableCredentials(params.rpId); - } - - if (cipherOptions.length === 0) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); - } - - const selectedCipherId = await userInterfaceSession.pickCredential( - cipherOptions.map((cipher) => cipher.id) - ); - const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId); - - if (selectedCipher === undefined) { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); - } - try { - const selectedFido2Key = - selectedCipher.type === CipherType.Login - ? selectedCipher.login.fido2Key - : selectedCipher.fido2Key; - const selectedCredentialId = - selectedCipher.type === CipherType.Login - ? selectedFido2Key.nonDiscoverableId - : selectedCipher.id; + if ( + params.requireUserVerification != undefined && + typeof params.requireUserVerification !== "boolean" + ) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } - ++selectedFido2Key.counter; + if (params.requireUserVerification) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); + } - selectedCipher.localData = { - ...selectedCipher.localData, - lastUsedDate: new Date().getTime(), - }; - const encrypted = await this.cipherService.encrypt(selectedCipher); - await this.cipherService.updateWithServer(encrypted); + let cipherOptions: CipherView[]; - const authenticatorData = await generateAuthData({ - rpId: selectedFido2Key.rpId, - credentialId: Utils.guidToRawFormat(selectedCredentialId), - counter: selectedFido2Key.counter, - userPresence: true, - userVerification: false, - }); + // eslint-disable-next-line no-empty + if (params.allowCredentialDescriptorList?.length > 0) { + cipherOptions = await this.findNonDiscoverableCredentials( + params.allowCredentialDescriptorList, + params.rpId + ); + } else { + cipherOptions = await this.findDiscoverableCredentials(params.rpId); + } - const signature = await generateSignature({ - authData: authenticatorData, - clientDataHash: params.hash, - privateKey: await getPrivateKeyFromFido2Key(selectedFido2Key), - }); + if (cipherOptions.length === 0) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); + } - return { - authenticatorData, - selectedCredential: { - id: Utils.guidToRawFormat(selectedCredentialId), - userHandle: Fido2Utils.stringToBuffer(selectedFido2Key.userHandle), - }, - signature, - }; - } catch { - throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + const selectedCipherId = await userInterfaceSession.pickCredential( + cipherOptions.map((cipher) => cipher.id) + ); + const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId); + + if (selectedCipher === undefined) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); + } + + try { + const selectedFido2Key = + selectedCipher.type === CipherType.Login + ? selectedCipher.login.fido2Key + : selectedCipher.fido2Key; + const selectedCredentialId = + selectedCipher.type === CipherType.Login + ? selectedFido2Key.nonDiscoverableId + : selectedCipher.id; + + ++selectedFido2Key.counter; + + selectedCipher.localData = { + ...selectedCipher.localData, + lastUsedDate: new Date().getTime(), + }; + const encrypted = await this.cipherService.encrypt(selectedCipher); + await this.cipherService.updateWithServer(encrypted); + + const authenticatorData = await generateAuthData({ + rpId: selectedFido2Key.rpId, + credentialId: Utils.guidToRawFormat(selectedCredentialId), + counter: selectedFido2Key.counter, + userPresence: true, + userVerification: false, + }); + + const signature = await generateSignature({ + authData: authenticatorData, + clientDataHash: params.hash, + privateKey: await getPrivateKeyFromFido2Key(selectedFido2Key), + }); + + return { + authenticatorData, + selectedCredential: { + id: Utils.guidToRawFormat(selectedCredentialId), + userHandle: Fido2Utils.stringToBuffer(selectedFido2Key.userHandle), + }, + signature, + }; + } catch { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } + } finally { + userInterfaceSession.close(); } }