1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[EC-598] feat: inform user when no credentials are found

This commit is contained in:
Andreas Coroiu
2023-04-17 11:20:25 +02:00
parent 1ab263e4d9
commit 69e36b972b
5 changed files with 43 additions and 9 deletions

View File

@@ -46,6 +46,14 @@
</div> </div>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="data.type == 'InformCredentialNotFoundRequest'">
You do not have a matching login for this site.
<div class="box list">
<div class="box-content">
<app-cipher-row *ngFor="let cipher of ciphers" [cipher]="cipher"></app-cipher-row>
</div>
</div>
</ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="abort(true)"> <button type="button" class="btn btn-outline-secondary" (click)="abort(true)">
Use browser built-in Use browser built-in
</button> </button>

View File

@@ -10,13 +10,13 @@ import {
takeUntil, takeUntil,
} from "rxjs"; } from "rxjs";
import { Utils } from "@bitwarden/common/misc/utils";
import { UserRequestedFallbackAbortReason } from "@bitwarden/common/fido2/abstractions/fido2-client.service.abstraction"; import { UserRequestedFallbackAbortReason } from "@bitwarden/common/fido2/abstractions/fido2-client.service.abstraction";
import { import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
Fido2UserInterfaceSession, Fido2UserInterfaceSession,
NewCredentialParams, NewCredentialParams,
} from "@bitwarden/common/fido2/abstractions/fido2-user-interface.service.abstraction"; } from "@bitwarden/common/fido2/abstractions/fido2-user-interface.service.abstraction";
import { Utils } from "@bitwarden/common/misc/utils";
import { BrowserApi } from "../../browser/browserApi"; import { BrowserApi } from "../../browser/browserApi";
import { PopupUtilsService } from "../../popup/services/popup-utils.service"; import { PopupUtilsService } from "../../popup/services/popup-utils.service";
@@ -79,6 +79,9 @@ export type BrowserFido2Message = { sessionId: string } & (
type: "InformExcludedCredentialRequest"; type: "InformExcludedCredentialRequest";
existingCipherIds: string[]; existingCipherIds: string[];
} }
| {
type: "InformCredentialNotFoundRequest";
}
| { | {
type: "AbortRequest"; type: "AbortRequest";
} }
@@ -240,6 +243,16 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
await this.receive("AbortResponse"); await this.receive("AbortResponse");
} }
async informCredentialNotFound(): Promise<void> {
const data: BrowserFido2Message = {
type: "InformCredentialNotFoundRequest",
sessionId: this.sessionId,
};
await this.send(data);
await this.receive("AbortResponse");
}
async close() { async close() {
await this.send({ type: "CloseRequest", sessionId: this.sessionId }); await this.send({ type: "CloseRequest", sessionId: this.sessionId });
this.closed = true; this.closed = true;

View File

@@ -25,5 +25,6 @@ export abstract class Fido2UserInterfaceSession {
existingCipherIds: string[], existingCipherIds: string[],
abortController?: AbortController abortController?: AbortController
) => Promise<void>; ) => Promise<void>;
informCredentialNotFound: (abortController?: AbortController) => Promise<void>;
close: () => void; close: () => void;
} }

View File

@@ -218,7 +218,7 @@ describe("FidoAuthenticatorService", () => {
}); });
/** Devation: Organization ciphers are not checked against excluded credentials, even if the user has access to them. */ /** Devation: Organization ciphers are not checked against excluded credentials, even if the user has access to them. */
it.only("should not inform user of duplication when the excluded credential belongs to an organization", async () => { it("should not inform user of duplication when the excluded credential belongs to an organization", async () => {
userInterfaceSession.informExcludedCredential.mockResolvedValue(); userInterfaceSession.informExcludedCredential.mockResolvedValue();
excludedCipherView.organizationId = "someOrganizationId"; excludedCipherView.organizationId = "someOrganizationId";
@@ -597,24 +597,35 @@ describe("FidoAuthenticatorService", () => {
}); });
}); });
/** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */ /**
it("should throw error if no credential exists", async () => { * Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation.
* Deviation: We do not throw error but instead inform the user and allow the user to fallback to browser implementation.
**/
it("should inform user if no credential exists", async () => {
cipherService.getAllDecrypted.mockResolvedValue([]); cipherService.getAllDecrypted.mockResolvedValue([]);
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
const result = async () => await authenticator.getAssertion(params); try {
await authenticator.getAssertion(params);
// eslint-disable-next-line no-empty
} catch {}
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed); expect(userInterfaceSession.informCredentialNotFound).toHaveBeenCalled();
}); });
it("should throw error if credential exists but rpId does not match", async () => { it("should inform user if credential exists but rpId does not match", async () => {
const cipher = await createCipherView({ type: CipherType.Login }); const cipher = await createCipherView({ type: CipherType.Login });
cipher.login.fido2Key.nonDiscoverableId = credentialId; cipher.login.fido2Key.nonDiscoverableId = credentialId;
cipher.login.fido2Key.rpId = "mismatch-rpid"; cipher.login.fido2Key.rpId = "mismatch-rpid";
cipherService.getAllDecrypted.mockResolvedValue([cipher]); cipherService.getAllDecrypted.mockResolvedValue([cipher]);
userInterfaceSession.informCredentialNotFound.mockResolvedValue();
const result = async () => await authenticator.getAssertion(params); try {
await authenticator.getAssertion(params);
// eslint-disable-next-line no-empty
} catch {}
await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.NotAllowed); expect(userInterfaceSession.informCredentialNotFound).toHaveBeenCalled();
}); });
}); });

View File

@@ -191,6 +191,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
} }
if (cipherOptions.length === 0) { if (cipherOptions.length === 0) {
await userInterfaceSession.informCredentialNotFound();
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
} }