mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 02:19:18 +00:00
Show existing login items in the UI
This commit is contained in:
committed by
Anders Åberg
parent
f3a392cf56
commit
e4221d4d56
@@ -49,6 +49,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"
|
||||
@Injectable()
|
||||
export class DesktopAutofillService implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private registrationRequest: autofill.PasskeyRegistrationRequest;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
@@ -184,8 +185,14 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
get lastRegistrationRequest() {
|
||||
return this.registrationRequest;
|
||||
}
|
||||
|
||||
listenIpc() {
|
||||
ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => {
|
||||
this.registrationRequest = request;
|
||||
|
||||
this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request);
|
||||
this.logService.warning(
|
||||
"listenPasskeyRegistration2",
|
||||
|
||||
@@ -3574,9 +3574,18 @@
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"closeBitwarden": {
|
||||
"message": "Close Bitwarden"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,10 @@ import { BitwardenShield } from "@bitwarden/auth/angular";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
compareCredentialIds,
|
||||
parseCredentialId,
|
||||
} from "@bitwarden/common/platform/services/fido2/credential-id-utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
@@ -24,6 +28,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
@@ -52,6 +57,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
|
||||
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
|
||||
containsExcludedCiphers: boolean = false;
|
||||
readonly Icons = { BitwardenShield };
|
||||
|
||||
constructor(
|
||||
@@ -59,6 +65,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy {
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly desktopAutofillService: DesktopAutofillService,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly domainSettingsService: DomainSettingsService,
|
||||
private readonly logService: LogService,
|
||||
@@ -69,6 +76,7 @@ export class Fido2CreateComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
await this.accountService.setShowHeader(false);
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest;
|
||||
const rpid = await this.session.getRpId();
|
||||
const equivalentDomains = await firstValueFrom(
|
||||
this.domainSettingsService.getUrlEquivalentDomains(rpid),
|
||||
@@ -80,16 +88,42 @@ export class Fido2CreateComponent implements OnInit, OnDestroy {
|
||||
this.cipherService
|
||||
.getAllDecrypted(activeUserId)
|
||||
.then((ciphers) => {
|
||||
const relevantCiphers = ciphers.filter((cipher) => {
|
||||
if (!cipher.login || !cipher.login.hasUris) {
|
||||
return false;
|
||||
}
|
||||
if (lastRegistrationRequest.excludedCredentials.length > 0) {
|
||||
const excludedCiphers = ciphers.filter((cipher) => {
|
||||
const credentialId = cipher.login.hasFido2Credentials
|
||||
? parseCredentialId(cipher.login.fido2Credentials[0]?.credentialId)
|
||||
: new Uint8Array();
|
||||
if (!cipher.login || !cipher.login.hasUris) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
cipher.login.matchesUri(rpid, equivalentDomains) && !cipher.login.hasFido2Credentials
|
||||
);
|
||||
});
|
||||
this.ciphersSubject.next(relevantCiphers);
|
||||
return (
|
||||
cipher.login.matchesUri(rpid, equivalentDomains) &&
|
||||
compareCredentialIds(
|
||||
credentialId,
|
||||
new Uint8Array(lastRegistrationRequest.excludedCredentials[0]),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
this.containsExcludedCiphers = excludedCiphers.length > 0;
|
||||
this.ciphersSubject.next(excludedCiphers);
|
||||
} else {
|
||||
const relevantCiphers = ciphers.filter((cipher) => {
|
||||
const credentialId = cipher.login.hasFido2Credentials
|
||||
? Array.from(parseCredentialId(cipher.login.fido2Credentials[0]?.credentialId))
|
||||
: [];
|
||||
if (!cipher.login || !cipher.login.hasUris) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
cipher.login.matchesUri(rpid, equivalentDomains) &&
|
||||
!lastRegistrationRequest.excludedCredentials.includes(credentialId)
|
||||
);
|
||||
});
|
||||
this.ciphersSubject.next(relevantCiphers);
|
||||
}
|
||||
})
|
||||
.catch((error) => this.logService.error(error));
|
||||
}
|
||||
@@ -99,11 +133,21 @@ export class Fido2CreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async addPasskeyToCipher(cipher: CipherView) {
|
||||
const userVerified = cipher.reprompt
|
||||
? await this.passwordRepromptService.showPasswordPrompt()
|
||||
: true;
|
||||
let isConfirmed = true;
|
||||
|
||||
this.session.notifyConfirmCreateCredential(userVerified, cipher);
|
||||
if (cipher.login.hasFido2Credentials) {
|
||||
isConfirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "overwritePasskey" },
|
||||
content: { key: "alreadyContainsPasskey" },
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
if (cipher.reprompt) {
|
||||
isConfirmed = await this.passwordRepromptService.showPasswordPrompt();
|
||||
}
|
||||
|
||||
this.session.notifyConfirmCreateCredential(isConfirmed, cipher);
|
||||
}
|
||||
|
||||
async confirmPasskey() {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</bit-section>
|
||||
|
||||
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col tw-grow">
|
||||
<div *ngIf="containsExcludedCiphers">{{ "passkeyAlreadyExists" | i18n }}</div>
|
||||
<bit-item *ngFor="let c of ciphers$ | async" class="">
|
||||
<button type="button" bit-item-content (click)="chooseCipher(c)">
|
||||
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
|
||||
|
||||
@@ -7,6 +7,10 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BitwardenShield } from "@bitwarden/auth/angular";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
compareCredentialIds,
|
||||
parseCredentialId,
|
||||
} from "@bitwarden/common/platform/services/fido2/credential-id-utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -23,6 +27,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
@@ -53,6 +58,7 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
|
||||
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
|
||||
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
|
||||
protected containsExcludedCiphers: boolean = false;
|
||||
cipherIds$: Observable<string[]>;
|
||||
readonly Icons = { BitwardenShield };
|
||||
|
||||
@@ -61,12 +67,14 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly desktopAutofillService: DesktopAutofillService,
|
||||
private readonly logService: LogService,
|
||||
private readonly passwordRepromptService: PasswordRepromptService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest;
|
||||
await this.accountService.setShowHeader(false);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
@@ -79,7 +87,27 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
this.cipherService
|
||||
.getAllDecryptedForIds(activeUserId, cipherIds || [])
|
||||
.then((ciphers) => {
|
||||
this.ciphersSubject.next(ciphers);
|
||||
if (lastRegistrationRequest) {
|
||||
const excludedCiphers = ciphers.filter((cipher) => {
|
||||
const credentialId = cipher.login.hasFido2Credentials
|
||||
? parseCredentialId(cipher.login.fido2Credentials[0]?.credentialId)
|
||||
: new Uint8Array();
|
||||
if (!cipher.login || !cipher.login.hasUris) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compareCredentialIds(
|
||||
credentialId,
|
||||
new Uint8Array(lastRegistrationRequest.excludedCredentials[0]),
|
||||
);
|
||||
});
|
||||
|
||||
this.containsExcludedCiphers = excludedCiphers.length > 0;
|
||||
|
||||
this.ciphersSubject.next(excludedCiphers || ciphers);
|
||||
} else {
|
||||
this.ciphersSubject.next(ciphers);
|
||||
}
|
||||
})
|
||||
.catch((error) => this.logService.error(error));
|
||||
});
|
||||
@@ -91,7 +119,9 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async chooseCipher(cipher: CipherView) {
|
||||
if (
|
||||
if (this.containsExcludedCiphers) {
|
||||
this.session?.confirmChosenCipher(cipher.id, false);
|
||||
} else if (
|
||||
cipher.reprompt !== CipherRepromptType.None &&
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export interface Fido2AuthenticatorGetAssertionParams {
|
||||
rpId: string;
|
||||
/** The hash of the serialized client data, provided by the client. */
|
||||
hash: BufferSource;
|
||||
allowCredentialDescriptorList: PublicKeyCredentialDescriptor[];
|
||||
allowCredentialDescriptorList?: PublicKeyCredentialDescriptor[];
|
||||
/** The effective user verification requirement for assertion, a Boolean value provided by the client. */
|
||||
requireUserVerification: boolean;
|
||||
/** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */
|
||||
|
||||
@@ -112,13 +112,18 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
const existingCipherIds = await this.findExcludedCredentials(
|
||||
params.excludeCredentialDescriptorList,
|
||||
);
|
||||
if (existingCipherIds.length > 0) {
|
||||
this.logService?.info(
|
||||
`[Fido2Authenticator] Aborting due to excluded credential found in vault.`,
|
||||
);
|
||||
await userInterfaceSession.informExcludedCredential(existingCipherIds);
|
||||
throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
}
|
||||
let cipherOptions: CipherView[];
|
||||
|
||||
await userInterfaceSession.ensureUnlockedVault();
|
||||
|
||||
const assertParams = { ...params, rpId: params.rpEntity.id, extensions: {} };
|
||||
|
||||
// Try to find the passkey locally before causing a sync to speed things up
|
||||
// only skip syncing if we found credentials AND all of them have a counter = 0
|
||||
const credentials = await this.findCredential(assertParams, cipherOptions);
|
||||
const masterPasswordRepromptRequired = credentials.some(
|
||||
(cipher) => cipher.reprompt !== CipherRepromptType.None,
|
||||
);
|
||||
|
||||
let cipher: CipherView;
|
||||
let fido2Credential: Fido2CredentialView;
|
||||
@@ -126,13 +131,31 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
let userVerified = false;
|
||||
let credentialId: string;
|
||||
let pubKeyDer: ArrayBuffer;
|
||||
const response = await userInterfaceSession.confirmNewCredential({
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.name,
|
||||
userHandle: Fido2Utils.bufferToString(params.userEntity.id),
|
||||
userVerification: params.requireUserVerification,
|
||||
rpId: params.rpEntity.id,
|
||||
});
|
||||
let response;
|
||||
|
||||
if (existingCipherIds.length > 0) {
|
||||
response = await userInterfaceSession.pickCredential({
|
||||
cipherIds: existingCipherIds,
|
||||
userVerification: params.requireUserVerification,
|
||||
assumeUserPresence: false,
|
||||
masterPasswordRepromptRequired,
|
||||
});
|
||||
|
||||
this.logService?.info(
|
||||
`[Fido2Authenticator] Aborting due to excluded credential found in vault.`,
|
||||
);
|
||||
await userInterfaceSession.informExcludedCredential(existingCipherIds);
|
||||
throw new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.NotAllowed);
|
||||
} else {
|
||||
response = await userInterfaceSession.confirmNewCredential({
|
||||
credentialName: params.rpEntity.name,
|
||||
userName: params.userEntity.name,
|
||||
userHandle: Fido2Utils.bufferToString(params.userEntity.id),
|
||||
userVerification: params.requireUserVerification,
|
||||
rpId: params.rpEntity.id,
|
||||
});
|
||||
}
|
||||
|
||||
const cipherId = response.cipherId;
|
||||
userVerified = response.userVerified;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user