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