mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
This reverts commit fcc2bc96d1.
This commit is contained in:
@@ -1763,8 +1763,14 @@
|
||||
"popupU2fCloseMessage": {
|
||||
"message": "This browser cannot process U2F requests in this popup window. Do you want to open this popup in a new window so that you can log in using U2F?"
|
||||
},
|
||||
"showIconsChangePasswordUrls": {
|
||||
"message": "Show website icons and retrieve change password URLs"
|
||||
"enableFavicon": {
|
||||
"message": "Show website icons"
|
||||
},
|
||||
"faviconDesc": {
|
||||
"message": "Show a recognizable image next to each login."
|
||||
},
|
||||
"faviconDescAlt": {
|
||||
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
|
||||
},
|
||||
"enableBadgeCounter": {
|
||||
"message": "Show badge counter"
|
||||
@@ -4373,7 +4379,7 @@
|
||||
},
|
||||
"uriMatchDefaultStrategyHint": {
|
||||
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||
@@ -5578,12 +5584,6 @@
|
||||
"message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.",
|
||||
"description": "Aria label for the body content of the generator nudge"
|
||||
},
|
||||
"aboutThisSetting": {
|
||||
"message": "About this setting"
|
||||
},
|
||||
"permitCipherDetailsDescription": {
|
||||
"message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service."
|
||||
},
|
||||
"noPermissionsViewPage": {
|
||||
"message": "You do not have permissions to view this page. Try logging in with a different account."
|
||||
},
|
||||
|
||||
@@ -45,10 +45,7 @@
|
||||
<bit-card>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="enableFavicon" type="checkbox" />
|
||||
<bit-label>
|
||||
{{ "showIconsChangePasswordUrls" | i18n }}
|
||||
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
|
||||
</bit-label>
|
||||
<bit-label>{{ "enableFavicon" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="showQuickCopyActions" type="checkbox" />
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Option,
|
||||
SelectModule,
|
||||
} from "@bitwarden/components";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupWidthOption } from "../../../platform/browser/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
@@ -47,7 +46,6 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto
|
||||
ReactiveFormsModule,
|
||||
CheckboxModule,
|
||||
BadgeModule,
|
||||
PermitCipherDetailsPopoverComponent,
|
||||
],
|
||||
})
|
||||
export class AppearanceV2Component implements OnInit {
|
||||
|
||||
@@ -162,15 +162,14 @@
|
||||
<input
|
||||
id="enableFavicons"
|
||||
type="checkbox"
|
||||
aria-describedby="enableFaviconsHelp"
|
||||
formControlName="enableFavicons"
|
||||
(change)="saveFavicons()"
|
||||
/>
|
||||
{{ "showIconsChangePasswordUrls" | i18n }}
|
||||
{{ "enableFavicon" | i18n }}
|
||||
</label>
|
||||
<div class="tw-inline-block tw-ml-2">
|
||||
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
|
||||
</div>
|
||||
</div>
|
||||
<small id="enableFaviconsHelp" class="help-block">{{ "faviconDesc" | i18n }}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,6 @@ import {
|
||||
BadgeComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
|
||||
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
||||
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
|
||||
@@ -86,7 +85,6 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
VaultTimeoutInputComponent,
|
||||
PermitCipherDetailsPopoverComponent,
|
||||
],
|
||||
})
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { SharedModule } from "./shared/shared.module";
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
|
||||
SharedModule,
|
||||
AppRoutingModule,
|
||||
VaultFilterModule,
|
||||
|
||||
@@ -1305,8 +1305,11 @@
|
||||
"message": "Automatically clear copied values from your clipboard.",
|
||||
"description": "Clipboard is the operating system thing where you copy/paste data to on your device."
|
||||
},
|
||||
"showIconsChangePasswordUrls": {
|
||||
"message": "Show website icons and retrieve change password URLs"
|
||||
"enableFavicon": {
|
||||
"message": "Show website icons"
|
||||
},
|
||||
"faviconDesc": {
|
||||
"message": "Show a recognizable image next to each login."
|
||||
},
|
||||
"enableMinToTray": {
|
||||
"message": "Minimize to tray icon"
|
||||
@@ -3931,12 +3934,6 @@
|
||||
"description": "Two part message",
|
||||
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
|
||||
},
|
||||
"aboutThisSetting": {
|
||||
"message": "About this setting"
|
||||
},
|
||||
"permitCipherDetailsDescription": {
|
||||
"message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service."
|
||||
},
|
||||
"assignToCollections": {
|
||||
"message": "Assign to collections"
|
||||
},
|
||||
|
||||
@@ -67,17 +67,23 @@
|
||||
</bit-select>
|
||||
<bit-hint>{{ "languageDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<div class="tw-flex tw-items-start tw-gap-1.5">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="enableFavicons" />
|
||||
<bit-label>
|
||||
{{ "showIconsChangePasswordUrls" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<div class="-tw-mt-0.5">
|
||||
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="enableFavicons" />
|
||||
<bit-label
|
||||
>{{ "enableFavicon" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/website-icons/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMoreAboutWebsiteIcons' | i18n }}"
|
||||
slot="end"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
<bit-hint>{{ "faviconDesc" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "theme" | i18n }}</bit-label>
|
||||
<bit-select formControlName="theme" id="theme">
|
||||
|
||||
@@ -34,7 +34,6 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { SharedModule } from "../shared";
|
||||
@@ -42,12 +41,7 @@ import { SharedModule } from "../shared";
|
||||
@Component({
|
||||
selector: "app-preferences",
|
||||
templateUrl: "preferences.component.html",
|
||||
imports: [
|
||||
SharedModule,
|
||||
HeaderModule,
|
||||
VaultTimeoutInputComponent,
|
||||
PermitCipherDetailsPopoverComponent,
|
||||
],
|
||||
imports: [SharedModule, HeaderModule, VaultTimeoutInputComponent],
|
||||
})
|
||||
export class PreferencesComponent implements OnInit, OnDestroy {
|
||||
// For use in template
|
||||
|
||||
@@ -2115,8 +2115,11 @@
|
||||
"languageDesc": {
|
||||
"message": "Change the language used by the web vault."
|
||||
},
|
||||
"showIconsChangePasswordUrls": {
|
||||
"message": "Show website icons and retrieve change password URLs"
|
||||
"enableFavicon": {
|
||||
"message": "Show website icons"
|
||||
},
|
||||
"faviconDesc": {
|
||||
"message": "Show a recognizable image next to each login."
|
||||
},
|
||||
"default": {
|
||||
"message": "Default"
|
||||
@@ -11072,12 +11075,6 @@
|
||||
"message": "Billing address required to add credit.",
|
||||
"description": "Error message shown when trying to add credit to a trialing organization without a billing address."
|
||||
},
|
||||
"aboutThisSetting": {
|
||||
"message": "About this setting"
|
||||
},
|
||||
"permitCipherDetailsDescription": {
|
||||
"message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service."
|
||||
},
|
||||
"billingAddress": {
|
||||
"message": "Billing address"
|
||||
},
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class ChangePasswordUriResponse extends BaseResponse {
|
||||
uri: string | null;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.uri = this.getResponseProperty("uri");
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<button
|
||||
type="button"
|
||||
class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-p-0"
|
||||
[bitPopoverTriggerFor]="permitDetailsPopover"
|
||||
position="above-center"
|
||||
[appA11yTitle]="'aboutThisSetting' | i18n"
|
||||
bitLink
|
||||
>
|
||||
<i class="bwi bwi-question-circle"></i>
|
||||
</button>
|
||||
|
||||
<bit-popover [title]="'aboutThisSetting' | i18n" #permitDetailsPopover>
|
||||
<p>
|
||||
{{ "permitCipherDetailsDescription" | i18n }}
|
||||
</p>
|
||||
<div class="tw-flex tw-gap-1.5 tw-items-center">
|
||||
<a bitLink href="#" (click)="openLearnMore($event)" class="tw-flex">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
<i slot="end" class="bwi bwi-external-link tw-text-primary-600" aria-hidden="true"></i>
|
||||
</div>
|
||||
</bit-popover>
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Component, inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { LinkModule, PopoverModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "vault-permit-cipher-details-popover",
|
||||
templateUrl: "./permit-cipher-details-popover.component.html",
|
||||
imports: [PopoverModule, JslibModule, LinkModule],
|
||||
})
|
||||
export class PermitCipherDetailsPopoverComponent {
|
||||
private platformUtilService = inject(PlatformUtilsService);
|
||||
|
||||
openLearnMore(e: Event) {
|
||||
e.preventDefault();
|
||||
this.platformUtilService.launchUri("https://bitwarden.com/help/website-icons/");
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export { openPasswordHistoryDialog } from "./components/password-history/passwor
|
||||
export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component";
|
||||
export * from "./components/carousel";
|
||||
export * from "./components/new-cipher-menu/new-cipher-menu.component";
|
||||
export * from "./components/permit-cipher-details-popover/permit-cipher-details-popover.component";
|
||||
|
||||
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
*/
|
||||
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
@@ -22,30 +18,37 @@ import { DefaultChangeLoginPasswordService } from "./default-change-login-passwo
|
||||
describe("DefaultChangeLoginPasswordService", () => {
|
||||
let service: DefaultChangeLoginPasswordService;
|
||||
|
||||
const mockApiService = mock<ApiService>();
|
||||
const mockDomainSettingsService = mock<DomainSettingsService>();
|
||||
let mockShouldNotExistResponse: Response;
|
||||
let mockWellKnownResponse: Response;
|
||||
|
||||
const showFavicons$ = new BehaviorSubject<boolean>(true);
|
||||
const getClientType = jest.fn(() => ClientType.Browser);
|
||||
|
||||
const mockApiService = mock<ApiService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>({
|
||||
getClientType,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiService.fetch.mockClear();
|
||||
mockApiService.fetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response),
|
||||
);
|
||||
mockApiService.nativeFetch.mockClear();
|
||||
|
||||
mockDomainSettingsService.showFavicons$ = showFavicons$;
|
||||
// Default responses to success state
|
||||
mockShouldNotExistResponse = new Response("Not Found", { status: 404 });
|
||||
mockWellKnownResponse = new Response("OK", { status: 200 });
|
||||
|
||||
const mockEnvironmentService = {
|
||||
environment$: of({
|
||||
getIconsUrl: () => "https://icons.bitwarden.com",
|
||||
} as Environment),
|
||||
} as EnvironmentService;
|
||||
mockApiService.nativeFetch.mockImplementation((request) => {
|
||||
if (
|
||||
request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200")
|
||||
) {
|
||||
return Promise.resolve(mockShouldNotExistResponse);
|
||||
}
|
||||
|
||||
service = new DefaultChangeLoginPasswordService(
|
||||
mockApiService,
|
||||
mockEnvironmentService,
|
||||
mockDomainSettingsService,
|
||||
);
|
||||
if (request.url.endsWith(".well-known/change-password")) {
|
||||
return Promise.resolve(mockWellKnownResponse);
|
||||
}
|
||||
|
||||
throw new Error("Unexpected request");
|
||||
});
|
||||
service = new DefaultChangeLoginPasswordService(mockApiService, platformUtilsService);
|
||||
});
|
||||
|
||||
it("should return null for non-login ciphers", async () => {
|
||||
@@ -82,7 +85,7 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
expect(url).toBeNull();
|
||||
});
|
||||
|
||||
it("should call the icons url endpoint", async () => {
|
||||
it("should check the origin for a reliable status code", async () => {
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
@@ -92,17 +95,45 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
|
||||
await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(mockApiService.fetch).toHaveBeenCalledWith(
|
||||
expect(mockApiService.nativeFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://icons.bitwarden.com/change-password-uri?uri=https%3A%2F%2Fexample.com%2F",
|
||||
url: "https://example.com/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the original URI when unable to verify the response", async () => {
|
||||
mockApiService.fetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response),
|
||||
it("should attempt to fetch the well-known change password URL", async () => {
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(mockApiService.nativeFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://example.com/.well-known/change-password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the well-known change password URL when successful at verifying the response", async () => {
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(url).toBe("https://example.com/.well-known/change-password");
|
||||
});
|
||||
|
||||
it("should return the original URI when unable to verify the response", async () => {
|
||||
mockShouldNotExistResponse = new Response("Ok", { status: 200 });
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
@@ -116,40 +147,34 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
expect(url).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should return the well known change url from the response", async () => {
|
||||
mockApiService.fetch.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uri: "https://example.com/.well-known/change-password" }),
|
||||
} as Response);
|
||||
});
|
||||
it("should return the original URI when the well-known URL is not found", async () => {
|
||||
mockWellKnownResponse = new Response("Not Found", { status: 404 });
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com/" }, { uri: "https://working.com/" }],
|
||||
uris: [{ uri: "https://example.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(url).toBe("https://example.com/.well-known/change-password");
|
||||
expect(url).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should try the next URI if the first one fails", async () => {
|
||||
mockApiService.fetch.mockImplementation((request) => {
|
||||
if (request.url.includes("no-wellknown.com")) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uri: null }),
|
||||
} as Response);
|
||||
mockApiService.nativeFetch.mockImplementation((request) => {
|
||||
if (
|
||||
request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200")
|
||||
) {
|
||||
return Promise.resolve(mockShouldNotExistResponse);
|
||||
}
|
||||
|
||||
if (request.url.includes("working.com")) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uri: "https://working.com/.well-known/change-password" }),
|
||||
} as Response);
|
||||
if (request.url.endsWith(".well-known/change-password")) {
|
||||
if (request.url.includes("working.com")) {
|
||||
return Promise.resolve(mockWellKnownResponse);
|
||||
}
|
||||
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
||||
}
|
||||
|
||||
throw new Error("Unexpected request");
|
||||
@@ -167,19 +192,19 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
expect(url).toBe("https://working.com/.well-known/change-password");
|
||||
});
|
||||
|
||||
it("returns the first URI when `showFavicons$` setting is disabled", async () => {
|
||||
showFavicons$.next(false);
|
||||
it("should return the first URI when the client type is not browser", async () => {
|
||||
getClientType.mockReturnValue(ClientType.Web);
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com/" }, { uri: "https://another.com/" }],
|
||||
uris: [{ uri: "https://example.com/" }, { uri: "https://example-2.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(mockApiService.nativeFetch).not.toHaveBeenCalled();
|
||||
expect(url).toBe("https://example.com/");
|
||||
expect(mockApiService.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ChangePasswordUriResponse } from "@bitwarden/common/vault/models/response/change-password-uri.response";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service";
|
||||
@@ -15,8 +12,7 @@ import { ChangeLoginPasswordService } from "../abstractions/change-login-passwor
|
||||
export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -37,19 +33,24 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer
|
||||
return null;
|
||||
}
|
||||
|
||||
const enableFaviconChangePassword = await firstValueFrom(
|
||||
this.domainSettingsService.showFavicons$,
|
||||
);
|
||||
|
||||
// When the setting is not enabled, return the first URL
|
||||
if (!enableFaviconChangePassword) {
|
||||
// CSP policies on the web and desktop restrict the application from making
|
||||
// cross-origin requests, breaking the below .well-known URL checks.
|
||||
// For those platforms, this will short circuit and return the first URL.
|
||||
// PM-21024 will build a solution for the server side to handle this.
|
||||
if (this.platformUtilsService.getClientType() !== "browser") {
|
||||
return urls[0].href;
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
const wellKnownChangeUrl = await this.fetchWellKnownChangePasswordUri(url.href);
|
||||
const [reliable, wellKnownChangeUrl] = await Promise.all([
|
||||
this.hasReliableHttpStatusCode(url.origin),
|
||||
this.getWellKnownChangePasswordUrl(url.origin),
|
||||
]);
|
||||
|
||||
if (wellKnownChangeUrl) {
|
||||
// Some servers return a 200 OK for a resource that should not exist
|
||||
// Which means we cannot trust the well-known URL is valid, so we skip it
|
||||
// to avoid potentially sending users to a 404 page
|
||||
if (reliable && wellKnownChangeUrl != null) {
|
||||
return wellKnownChangeUrl;
|
||||
}
|
||||
}
|
||||
@@ -59,41 +60,55 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the well-known change-password-uri for the given URL.
|
||||
* @returns The full URL to the change password page, or null if it could not be found.
|
||||
* Checks if the server returns a non-200 status code for a resource that should not exist.
|
||||
* See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics
|
||||
* @param urlOrigin The origin of the URL to check
|
||||
*/
|
||||
private async fetchWellKnownChangePasswordUri(url: string): Promise<string | null> {
|
||||
const getChangePasswordUriRequest = await this.buildChangePasswordUriRequest(url);
|
||||
private async hasReliableHttpStatusCode(urlOrigin: string): Promise<boolean> {
|
||||
try {
|
||||
const url = new URL(
|
||||
"./.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200",
|
||||
urlOrigin,
|
||||
);
|
||||
|
||||
const response = await this.apiService.fetch(getChangePasswordUriRequest);
|
||||
const request = new Request(url, {
|
||||
method: "GET",
|
||||
mode: "same-origin",
|
||||
credentials: "omit",
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
const response = await this.apiService.nativeFetch(request);
|
||||
return !response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const { uri } = new ChangePasswordUriResponse(data);
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the request for the change-password-uri endpoint.
|
||||
* Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response
|
||||
* is returned. Returns null if the request throws or the response is not 200 OK.
|
||||
* See https://w3c.github.io/webappsec-change-password-url/
|
||||
* @param urlOrigin The origin of the URL to check
|
||||
*/
|
||||
private async buildChangePasswordUriRequest(cipherUri: string): Promise<Request> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("uri", cipherUri);
|
||||
private async getWellKnownChangePasswordUrl(urlOrigin: string): Promise<string | null> {
|
||||
try {
|
||||
const url = new URL("./.well-known/change-password", urlOrigin);
|
||||
|
||||
// The change-password-uri endpoint lives within the icons service
|
||||
// as it uses decrypted cipher data.
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const iconsUrl = env.getIconsUrl();
|
||||
const request = new Request(url, {
|
||||
method: "GET",
|
||||
mode: "same-origin",
|
||||
credentials: "omit",
|
||||
cache: "no-store",
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
const url = new URL(`${iconsUrl}/change-password-uri?${searchParams.toString()}`);
|
||||
const response = await this.apiService.nativeFetch(request);
|
||||
|
||||
return new Request(url, {
|
||||
method: "GET",
|
||||
});
|
||||
return response.ok ? url.toString() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user