mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
Merge branch 'main' into autofill/pm-8518-autofill-scripts-do-not-inject-into-sub-frames-on-install
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
|
||||
export class AutofillTabCommand {
|
||||
constructor(private autofillService: AutofillService) {}
|
||||
|
||||
async doAutofillTabCommand(tab: chrome.tabs.Tab) {
|
||||
if (!tab.id) {
|
||||
throw new Error("Tab does not have an id, cannot complete autofill.");
|
||||
}
|
||||
|
||||
const details = await this.collectPageDetails(tab.id);
|
||||
await this.autofillService.doAutoFillOnTab(
|
||||
[
|
||||
{
|
||||
frameId: 0,
|
||||
tab: tab,
|
||||
details: details,
|
||||
},
|
||||
],
|
||||
tab,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async doAutofillTabWithCipherCommand(tab: chrome.tabs.Tab, cipher: CipherView) {
|
||||
if (!tab.id) {
|
||||
throw new Error("Tab does not have an id, cannot complete autofill.");
|
||||
}
|
||||
|
||||
const details = await this.collectPageDetails(tab.id);
|
||||
await this.autofillService.doAutoFill({
|
||||
tab: tab,
|
||||
cipher: cipher,
|
||||
pageDetails: [
|
||||
{
|
||||
frameId: 0,
|
||||
tab: tab,
|
||||
details: details,
|
||||
},
|
||||
],
|
||||
skipLastUsed: false,
|
||||
skipUsernameOnlyFill: false,
|
||||
onlyEmptyFields: false,
|
||||
onlyVisibleFields: false,
|
||||
fillNewPassword: true,
|
||||
allowTotpAutofill: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async collectPageDetails(tabId: number): Promise<AutofillPageDetails> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(
|
||||
tabId,
|
||||
{
|
||||
command: "collectPageDetailsImmediately",
|
||||
},
|
||||
(response: AutofillPageDetails) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
14
apps/browser/src/autofill/enums/autofill-message.enums.ts
Normal file
14
apps/browser/src/autofill/enums/autofill-message.enums.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const AutofillMessageCommand = {
|
||||
collectPageDetails: "collectPageDetails",
|
||||
collectPageDetailsResponse: "collectPageDetailsResponse",
|
||||
} as const;
|
||||
|
||||
export type AutofillMessageCommandType =
|
||||
(typeof AutofillMessageCommand)[keyof typeof AutofillMessageCommand];
|
||||
|
||||
export const AutofillMessageSender = {
|
||||
collectPageDetailsFromTabObservable: "collectPageDetailsFromTabObservable",
|
||||
} as const;
|
||||
|
||||
export type AutofillMessageSenderType =
|
||||
(typeof AutofillMessageSender)[keyof typeof AutofillMessageSender];
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { CommandDefinition } from "@bitwarden/common/platform/messaging";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { AutofillMessageCommand } from "../../enums/autofill-message.enums";
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
import AutofillForm from "../../models/autofill-form";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
@@ -44,7 +48,20 @@ export interface GenerateFillScriptOptions {
|
||||
defaultUriMatch: UriMatchStrategySetting;
|
||||
}
|
||||
|
||||
export type CollectPageDetailsResponseMessage = {
|
||||
tab: chrome.tabs.Tab;
|
||||
details: AutofillPageDetails;
|
||||
sender?: string;
|
||||
webExtSender: chrome.runtime.MessageSender;
|
||||
};
|
||||
|
||||
export const COLLECT_PAGE_DETAILS_RESPONSE_COMMAND =
|
||||
new CommandDefinition<CollectPageDetailsResponseMessage>(
|
||||
AutofillMessageCommand.collectPageDetailsResponse,
|
||||
);
|
||||
|
||||
export abstract class AutofillService {
|
||||
collectPageDetailsFromTab$: (tab: chrome.tabs.Tab) => Observable<PageDetail[]>;
|
||||
loadAutofillScriptsOnInstall: () => Promise<void>;
|
||||
reloadAutofillScripts: () => Promise<void>;
|
||||
injectAutofillScripts: (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock, mockReset, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BehaviorSubject, of, Subject } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -16,12 +16,14 @@ import { EventType } from "@bitwarden/common/enums";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
subscribeTo,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -37,6 +39,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -52,6 +55,7 @@ import { flushPromises, triggerTestFailure } from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
AutoFillOptions,
|
||||
CollectPageDetailsResponseMessage,
|
||||
GenerateFillScriptOptions,
|
||||
PageDetail,
|
||||
} from "./abstractions/autofill.service";
|
||||
@@ -82,6 +86,7 @@ describe("AutofillService", () => {
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let messageListener: MockProxy<MessageListener>;
|
||||
|
||||
beforeEach(() => {
|
||||
scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService);
|
||||
@@ -91,6 +96,7 @@ describe("AutofillService", () => {
|
||||
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||
authService = mock<AuthService>();
|
||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||
messageListener = mock<MessageListener>();
|
||||
autofillService = new AutofillService(
|
||||
cipherService,
|
||||
autofillSettingsService,
|
||||
@@ -103,10 +109,11 @@ describe("AutofillService", () => {
|
||||
scriptInjectorService,
|
||||
accountService,
|
||||
authService,
|
||||
messageListener,
|
||||
);
|
||||
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||
jest.spyOn(BrowserApi, "tabSendMessage");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -114,6 +121,84 @@ describe("AutofillService", () => {
|
||||
mockReset(cipherService);
|
||||
});
|
||||
|
||||
describe("collectPageDetailsFromTab$", () => {
|
||||
const tab = mock<chrome.tabs.Tab>({ id: 1 });
|
||||
const messages = new Subject<CollectPageDetailsResponseMessage>();
|
||||
|
||||
function mockCollectPageDetailsResponseMessage(
|
||||
tab: chrome.tabs.Tab,
|
||||
webExtSender: chrome.runtime.MessageSender = mock<chrome.runtime.MessageSender>(),
|
||||
sender: string = AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
): CollectPageDetailsResponseMessage {
|
||||
return mock<CollectPageDetailsResponseMessage>({
|
||||
tab,
|
||||
webExtSender,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
messageListener.messages$.mockReturnValue(messages.asObservable());
|
||||
});
|
||||
|
||||
it("sends a `collectPageDetails` message to the passed tab", () => {
|
||||
autofillService.collectPageDetailsFromTab$(tab);
|
||||
|
||||
expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(tab, {
|
||||
command: AutofillMessageCommand.collectPageDetails,
|
||||
sender: AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
tab,
|
||||
});
|
||||
});
|
||||
|
||||
it("builds an array of page details from received `collectPageDetailsResponse` messages", async () => {
|
||||
const topLevelSender = mock<chrome.runtime.MessageSender>({ tab, frameId: 0 });
|
||||
const subFrameSender = mock<chrome.runtime.MessageSender>({ tab, frameId: 1 });
|
||||
|
||||
const tracker = subscribeTo(autofillService.collectPageDetailsFromTab$(tab));
|
||||
const pausePromise = tracker.pauseUntilReceived(2);
|
||||
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab, topLevelSender));
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab, subFrameSender));
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[1].length).toBe(2);
|
||||
});
|
||||
|
||||
it("ignores messages from a different tab", async () => {
|
||||
const otherTab = mock<chrome.tabs.Tab>({ id: 2 });
|
||||
|
||||
const tracker = subscribeTo(autofillService.collectPageDetailsFromTab$(tab));
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab));
|
||||
messages.next(mockCollectPageDetailsResponseMessage(otherTab));
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores messages from a different sender", async () => {
|
||||
const tracker = subscribeTo(autofillService.collectPageDetailsFromTab$(tab));
|
||||
const pausePromise = tracker.pauseUntilReceived(1);
|
||||
|
||||
messages.next(mockCollectPageDetailsResponseMessage(tab));
|
||||
messages.next(
|
||||
mockCollectPageDetailsResponseMessage(
|
||||
tab,
|
||||
mock<chrome.runtime.MessageSender>(),
|
||||
"some-other-sender",
|
||||
),
|
||||
);
|
||||
|
||||
await pausePromise;
|
||||
|
||||
expect(tracker.emissions[1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadAutofillScriptsOnInstall", () => {
|
||||
let tab1: chrome.tabs.Tab;
|
||||
let tab2: chrome.tabs.Tab;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { firstValueFrom, startWith } from "rxjs";
|
||||
import { filter, firstValueFrom, Observable, scan, startWith } from "rxjs";
|
||||
import { pairwise } from "rxjs/operators";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
UriMatchStrategy,
|
||||
} from "@bitwarden/common/models/domain/domain-service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -27,6 +28,7 @@ import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@@ -35,6 +37,7 @@ import AutofillScript from "../models/autofill-script";
|
||||
import {
|
||||
AutoFillOptions,
|
||||
AutofillService as AutofillServiceInterface,
|
||||
COLLECT_PAGE_DETAILS_RESPONSE_COMMAND,
|
||||
FormData,
|
||||
GenerateFillScriptOptions,
|
||||
PageDetail,
|
||||
@@ -64,8 +67,47 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
private scriptInjectorService: ScriptInjectorService,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private messageListener: MessageListener,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Collects page details from the specific tab. This method returns an observable that can
|
||||
* be subscribed to in order to build the results from all collectPageDetailsResponse
|
||||
* messages from the given tab.
|
||||
*
|
||||
* @param tab The tab to collect page details from
|
||||
*/
|
||||
collectPageDetailsFromTab$(tab: chrome.tabs.Tab): Observable<PageDetail[]> {
|
||||
const pageDetailsFromTab$ = this.messageListener
|
||||
.messages$(COLLECT_PAGE_DETAILS_RESPONSE_COMMAND)
|
||||
.pipe(
|
||||
filter(
|
||||
(message) =>
|
||||
message.tab.id === tab.id &&
|
||||
message.sender === AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
),
|
||||
scan(
|
||||
(acc, message) => [
|
||||
...acc,
|
||||
{
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
},
|
||||
],
|
||||
[] as PageDetail[],
|
||||
),
|
||||
);
|
||||
|
||||
void BrowserApi.tabSendMessage(tab, {
|
||||
tab: tab,
|
||||
command: AutofillMessageCommand.collectPageDetails,
|
||||
sender: AutofillMessageSender.collectPageDetailsFromTabObservable,
|
||||
});
|
||||
|
||||
return pageDetailsFromTab$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers on installation of the extension Handles injecting
|
||||
* content scripts into all tabs that are currently open, and
|
||||
|
||||
@@ -889,6 +889,7 @@ export default class MainBackground {
|
||||
this.scriptInjectorService,
|
||||
this.accountService,
|
||||
this.authService,
|
||||
messageListener,
|
||||
);
|
||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||
|
||||
|
||||
@@ -342,6 +342,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ScriptInjectorService,
|
||||
AccountServiceAbstraction,
|
||||
AuthService,
|
||||
MessageListener,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, from } from "rxjs";
|
||||
import { Subject, firstValueFrom, from, Subscription } from "rxjs";
|
||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
|
||||
@@ -51,12 +51,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
autofillCalloutText: string;
|
||||
protected search$ = new Subject<void>();
|
||||
private destroy$ = new Subject<void>();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private totpCode: string;
|
||||
private totpTimeout: number;
|
||||
private loadedTimeout: number;
|
||||
private searchTimeout: number;
|
||||
private initPageDetailsTimeout: number;
|
||||
|
||||
protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.UnassignedItemsBanner,
|
||||
@@ -100,15 +100,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
case "collectPageDetailsResponse":
|
||||
if (message.sender === BroadcasterSubscriptionId) {
|
||||
this.pageDetails.push({
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -266,6 +257,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
protected async load() {
|
||||
this.isLoading = false;
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
if (this.tab != null) {
|
||||
this.url = this.tab.url;
|
||||
} else {
|
||||
@@ -274,8 +266,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hostname = Utils.getHostname(this.url);
|
||||
this.pageDetails = [];
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
|
||||
this.hostname = Utils.getHostname(this.url);
|
||||
const otherTypes: CipherType[] = [];
|
||||
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
|
||||
const dontShowIdentities = !(await firstValueFrom(
|
||||
@@ -323,7 +321,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.isLoading = this.loaded = true;
|
||||
this.collectTabPageDetails();
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
@@ -361,19 +358,4 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
|
||||
}
|
||||
}
|
||||
|
||||
private collectTabPageDetails() {
|
||||
void BrowserApi.tabSendMessage(this.tab, {
|
||||
command: "collectPageDetails",
|
||||
tab: this.tab,
|
||||
sender: BroadcasterSubscriptionId,
|
||||
});
|
||||
|
||||
window.clearTimeout(this.initPageDetailsTimeout);
|
||||
this.initPageDetailsTimeout = window.setTimeout(() => {
|
||||
if (this.pageDetails.length === 0) {
|
||||
this.collectTabPageDetails();
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DatePipe, Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
|
||||
@@ -68,6 +68,7 @@ export class ViewComponent extends BaseViewComponent {
|
||||
inPopout = false;
|
||||
cipherType = CipherType;
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -152,15 +153,6 @@ export class ViewComponent extends BaseViewComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "collectPageDetailsResponse":
|
||||
if (message.sender === BroadcasterSubscriptionId) {
|
||||
this.pageDetails.push({
|
||||
frameId: message.webExtSender.frameId,
|
||||
tab: message.tab,
|
||||
details: message.details,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "tabChanged":
|
||||
case "windowChanged":
|
||||
if (this.loadPageDetailsTimeout != null) {
|
||||
@@ -337,6 +329,7 @@ export class ViewComponent extends BaseViewComponent {
|
||||
}
|
||||
|
||||
private async loadPageDetails() {
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.pageDetails = [];
|
||||
this.tab = this.senderTabId
|
||||
? await BrowserApi.getTab(this.senderTabId)
|
||||
@@ -346,13 +339,10 @@ export class ViewComponent extends BaseViewComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.tabSendMessage(this.tab, {
|
||||
command: "collectPageDetails",
|
||||
tab: this.tab,
|
||||
sender: BroadcasterSubscriptionId,
|
||||
});
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
}
|
||||
|
||||
private async doAutofill() {
|
||||
|
||||
@@ -753,6 +753,7 @@ export class ServiceContainer {
|
||||
|
||||
await this.stateService.clean();
|
||||
await this.accountService.clean(userId);
|
||||
await this.accountService.switchAccount(null);
|
||||
process.env.BW_SESSION = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<div class="tw-mb-3 tw-flex tw-items-center">
|
||||
<button bitButton type="button" buttonType="primary" [bitAction]="sendEmail">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
<span class="tw-text-success tw-ml-3" *ngIf="sentEmail">
|
||||
{{ "verificationCodeEmailSent" | i18n: sentEmail }}
|
||||
{{ "emailSent" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
|
||||
@@ -31,7 +31,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
emailPromise: Promise<unknown>;
|
||||
override componentName = "app-two-factor-email";
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [null],
|
||||
token: ["", [Validators.required]],
|
||||
email: ["", [Validators.email, Validators.required]],
|
||||
});
|
||||
|
||||
@@ -79,6 +79,10 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
if (this.enabled) {
|
||||
await this.disableEmail();
|
||||
this.onChangeStatus.emit(false);
|
||||
|
||||
@@ -1,173 +1,124 @@
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
class="container"
|
||||
ngNativeValidate
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div
|
||||
class="col-5"
|
||||
[ngClass]="{
|
||||
'col-9': !duoFrameless && isDuoProvider
|
||||
}"
|
||||
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
|
||||
<div class="tw-min-w-96">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Email ||
|
||||
selectedProviderType === providerType.Authenticator
|
||||
"
|
||||
>
|
||||
<p class="lead text-center mb-4">{{ title }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
selectedProviderType === providerType.Email ||
|
||||
selectedProviderType === providerType.Authenticator
|
||||
"
|
||||
<p bitTypography="body1" *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1" *ngIf="selectedProviderType === providerType.Email">
|
||||
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="token" appAutofocus appInputVerbatim />
|
||||
<bit-hint *ngIf="selectedProviderType === providerType.Email">
|
||||
<a
|
||||
bitLink
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="sendEmail(true)"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
>
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
name="Code"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="tel"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="sendEmail(true)"
|
||||
[appApiAction]="emailPromise"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
>
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p class="text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="" />
|
||||
</picture>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
|
||||
<input
|
||||
id="code"
|
||||
type="password"
|
||||
name="Code"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="mb-3">
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Duo -->
|
||||
<ng-container *ngIf="isDuoProvider">
|
||||
<ng-container *ngIf="duoFrameless">
|
||||
<p *ngIf="selectedProviderType === providerType.OrganizationDuo" class="tw-mb-0">
|
||||
{{ "duoRequiredByOrgForAccount" | i18n }}
|
||||
</p>
|
||||
<p>{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!duoFrameless">
|
||||
<div id="duo-frame" class="mb-3">
|
||||
<iframe
|
||||
id="duo_iframe"
|
||||
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<i
|
||||
class="bwi bwi-spinner text-muted bwi-spin pull-right"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input
|
||||
id="remember"
|
||||
type="checkbox"
|
||||
name="Remember"
|
||||
class="form-check-input"
|
||||
[(ngModel)]="remember"
|
||||
/>
|
||||
<label for="remember" class="form-check-label">{{ "rememberMe" | i18n }}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p>{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p>{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe
|
||||
id="hcaptcha_iframe"
|
||||
height="80"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="tw-flex tw-flex-col tw-mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="
|
||||
selectedProviderType != null &&
|
||||
!isDuoProvider &&
|
||||
selectedProviderType !== providerType.WebAuthn
|
||||
"
|
||||
>
|
||||
<span>
|
||||
<i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}
|
||||
</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
(click)="launchDuoFrameless()"
|
||||
type="button"
|
||||
class="btn btn-primary btn-block"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="duoFrameless && isDuoProvider"
|
||||
>
|
||||
<span> {{ "launchDuo" | i18n }} </span>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a href="#" appStopClick (click)="anotherMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a></bit-hint
|
||||
>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="tw-rounded img-fluid tw-mb-3" alt="" />
|
||||
</picture>
|
||||
<bit-form-field>
|
||||
<bit-label class="tw-sr-only">{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input
|
||||
type="text"
|
||||
bitInput
|
||||
formControlName="token"
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
|
||||
<div id="web-authn-frame" class="tw-mb-3">
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Duo -->
|
||||
<ng-container *ngIf="isDuoProvider">
|
||||
<ng-container *ngIf="duoFrameless">
|
||||
<p
|
||||
bitTypography="body1"
|
||||
*ngIf="selectedProviderType === providerType.OrganizationDuo"
|
||||
class="tw-mb-0"
|
||||
>
|
||||
{{ "duoRequiredByOrgForAccount" | i18n }}
|
||||
</p>
|
||||
<p bitTypography="body1">{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!duoFrameless">
|
||||
<div id="duo-frame" class="tw-mb-3">
|
||||
<iframe
|
||||
id="duo_iframe"
|
||||
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
||||
></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<bit-form-control *ngIf="selectedProviderType != null">
|
||||
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="remember" />
|
||||
</bit-form-control>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="
|
||||
selectedProviderType != null &&
|
||||
!isDuoProvider &&
|
||||
selectedProviderType !== providerType.WebAuthn
|
||||
"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
<button
|
||||
(click)="launchDuoFrameless()"
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="duoFrameless && isDuoProvider"
|
||||
>
|
||||
<span> {{ "launchDuo" | i18n }} </span>
|
||||
</button>
|
||||
<a routerLink="/login" bitButton buttonType="secondary">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="anotherMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, Inject, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { Subject, takeUntil, lastValueFrom } from "rxjs";
|
||||
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
@@ -38,7 +39,17 @@ import {
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDestroy {
|
||||
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
|
||||
twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.required],
|
||||
updateOn: "submit",
|
||||
},
|
||||
],
|
||||
remember: [false],
|
||||
});
|
||||
private destroy$ = new Subject<void>();
|
||||
constructor(
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
@@ -58,6 +69,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
||||
configService: ConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
) {
|
||||
super(
|
||||
@@ -82,6 +94,16 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
|
||||
);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.token = value.token;
|
||||
this.remember = value.remember;
|
||||
});
|
||||
}
|
||||
submitForm = async () => {
|
||||
await this.submit();
|
||||
};
|
||||
|
||||
async anotherMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||
|
||||
@@ -82,7 +82,6 @@ const routes: Routes = [
|
||||
component: LoginViaAuthRequestComponent,
|
||||
data: { titleId: "adminApprovalRequested" } satisfies DataProperties,
|
||||
},
|
||||
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
|
||||
{
|
||||
path: "login-initiated",
|
||||
component: LoginDecryptionOptionsComponent,
|
||||
@@ -189,6 +188,33 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "2fa",
|
||||
component: TwoFactorComponent,
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: "verifyIdentity",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "recover-2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RecoverTwoFactorComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "recoverAccountTwoStep",
|
||||
titleId: "recoverAccountTwoStep",
|
||||
} satisfies DataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "accept-emergency",
|
||||
canActivate: [deepLinkGuard()],
|
||||
@@ -212,25 +238,6 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "recover-2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RecoverTwoFactorComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "recoverAccountTwoStep",
|
||||
titleId: "recoverAccountTwoStep",
|
||||
} satisfies DataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
|
||||
@@ -722,6 +722,9 @@
|
||||
"logIn": {
|
||||
"message": "Log in"
|
||||
},
|
||||
"verifyIdentity": {
|
||||
"message": "Verify your Identity"
|
||||
},
|
||||
"logInInitiated": {
|
||||
"message": "Log in initiated"
|
||||
},
|
||||
@@ -8330,5 +8333,12 @@
|
||||
},
|
||||
"viewSecret": {
|
||||
"message": "View secret"
|
||||
},
|
||||
"noClients": {
|
||||
"message": "There are no clients to list"
|
||||
},
|
||||
"providerBillingEmailHint": {
|
||||
"message": "This email address will receive all invoices pertaining to this provider",
|
||||
"description": "A hint that shows up on the Provider setup page to inform the admin the billing email will receive the provider's invoices."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user