diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index b09b0b7b67b..a44b1e11c32 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -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." }, diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance-v2.component.html index c9598c76db0..4f7f2757e0e 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.html +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.html @@ -45,10 +45,7 @@ - - {{ "showIconsChangePasswordUrls" | i18n }} - - + {{ "enableFavicon" | i18n }} diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index 23a609bd008..d998ef846d2 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -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 { diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 4af12903a24..6ebaea550ec 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -162,15 +162,14 @@ - {{ "showIconsChangePasswordUrls" | i18n }} + {{ "enableFavicon" | i18n }} -
- -
+ {{ "faviconDesc" | i18n }} diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 069cfb095ed..98d11469d1e 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -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 { diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 4f53e587994..014e29555e8 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -31,6 +31,7 @@ import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserAnimationsModule, + SharedModule, AppRoutingModule, VaultFilterModule, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index c2df4e280c0..66ec19747b7 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -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" }, diff --git a/apps/web/src/app/settings/preferences.component.html b/apps/web/src/app/settings/preferences.component.html index 050d7395caf..80261ecccb7 100644 --- a/apps/web/src/app/settings/preferences.component.html +++ b/apps/web/src/app/settings/preferences.component.html @@ -67,17 +67,23 @@ {{ "languageDesc" | i18n }} -
- - - - {{ "showIconsChangePasswordUrls" | i18n }} - - -
- -
-
+ + + {{ "enableFavicon" | i18n }} + + + + + {{ "faviconDesc" | i18n }} + {{ "theme" | i18n }} diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index 58a072ce76a..e6cc35903a7 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -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 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 937da72f7c2..96f47a35f57 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -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" }, diff --git a/libs/common/src/vault/models/response/change-password-uri.response.ts b/libs/common/src/vault/models/response/change-password-uri.response.ts deleted file mode 100644 index 1ff3424a269..00000000000 --- a/libs/common/src/vault/models/response/change-password-uri.response.ts +++ /dev/null @@ -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"); - } -} diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html deleted file mode 100644 index 1833a148616..00000000000 --- a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - - -

- {{ "permitCipherDetailsDescription" | i18n }} -

- -
diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts deleted file mode 100644 index 8e80ddf7810..00000000000 --- a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts +++ /dev/null @@ -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/"); - } -} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index efaefc77ade..f3925ac3379 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -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"; diff --git a/libs/vault/src/services/default-change-login-password.service.spec.ts b/libs/vault/src/services/default-change-login-password.service.spec.ts index 42242f2e4a8..c9628797f4d 100644 --- a/libs/vault/src/services/default-change-login-password.service.spec.ts +++ b/libs/vault/src/services/default-change-login-password.service.spec.ts @@ -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(); - const mockDomainSettingsService = mock(); + let mockShouldNotExistResponse: Response; + let mockWellKnownResponse: Response; - const showFavicons$ = new BehaviorSubject(true); + const getClientType = jest.fn(() => ClientType.Browser); + + const mockApiService = mock(); + const platformUtilsService = mock({ + 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(); }); }); diff --git a/libs/vault/src/services/default-change-login-password.service.ts b/libs/vault/src/services/default-change-login-password.service.ts index 929f5819c02..a0b5646c5a9 100644 --- a/libs/vault/src/services/default-change-login-password.service.ts +++ b/libs/vault/src/services/default-change-login-password.service.ts @@ -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 { - const getChangePasswordUriRequest = await this.buildChangePasswordUriRequest(url); + private async hasReliableHttpStatusCode(urlOrigin: string): Promise { + 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 { - const searchParams = new URLSearchParams(); - searchParams.set("uri", cipherUri); + private async getWellKnownChangePasswordUrl(urlOrigin: string): Promise { + 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; + } } }