mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
[EC-598] feat: inform user when no credentials are found
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user