From 678694dc6a5554e653e04d10ede5446bda3e8333 Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Wed, 11 Jun 2025 12:10:19 -0400 Subject: [PATCH] Allow change-password-service to be invoked in non-Angular contexts. --- .../background/notification.background.ts | 8 +- ...ification-change-login-password.service.ts | 99 ------------------- .../default-change-login-password.service.ts | 31 ++++-- 3 files changed, 30 insertions(+), 108 deletions(-) delete mode 100644 apps/browser/src/autofill/services/notification-change-login-password.service.ts diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 3c63d423aaa..ab570dc5361 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -43,6 +43,7 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { TaskService } from "@bitwarden/common/vault/tasks"; import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums"; import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task"; +import { DefaultChangeLoginPasswordService } from "@bitwarden/vault"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -58,7 +59,6 @@ import { import { CollectionView } from "../content/components/common-types"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; -import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service"; import { AddChangePasswordQueueMessage, @@ -401,8 +401,10 @@ export default class NotificationBackground { ): Promise { const { activeUserId, securityTask, cipher } = message.data; const domain = Utils.getDomain(sender.tab.url); - const passwordChangeUri = - await new TemporaryNotificationChangeLoginService().getChangePasswordUrl(cipher); + const passwordChangeUri = await DefaultChangeLoginPasswordService.create( + { nativeFetch: fetch }, + { getClientType: () => "browser" }, + ).getChangePasswordUrl(cipher); const authStatus = await this.getAuthStatus(); diff --git a/apps/browser/src/autofill/services/notification-change-login-password.service.ts b/apps/browser/src/autofill/services/notification-change-login-password.service.ts deleted file mode 100644 index f7b8c2cfb9c..00000000000 --- a/apps/browser/src/autofill/services/notification-change-login-password.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -// Duplicates Default Change Login Password Service, for now -// Since the former is an Angular injectable service, and we -// need to use the function inside of lit components. -// If primary service can be abstracted, that would be ideal. - -export class TemporaryNotificationChangeLoginService { - async getChangePasswordUrl(cipher: CipherView, fallback = false): Promise { - // Ensure we have a cipher with at least one URI - if (cipher.type !== CipherType.Login || cipher.login == null || !cipher.login.hasUris) { - return null; - } - - // Filter for valid URLs that are HTTP(S) - const urls = cipher.login.uris - .map((m) => Utils.getUrl(m.uri)) - .filter((m) => m != null && (m.protocol === "http:" || m.protocol === "https:")); - - if (urls.length === 0) { - return null; - } - - for (const url of urls) { - const [reliable, wellKnownChangeUrl] = await Promise.all([ - this.hasReliableHttpStatusCode(url.origin), - this.getWellKnownChangePasswordUrl(url.origin), - ]); - - // 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; - } - } - - // No reliable well-known URL found, fallback to the first URL - - // @TODO reimplement option in original service to indicate if no URL found. - // return urls[0].href; (originally) - return fallback ? urls[0].href : null; - } - - /** - * 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 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 request = new Request(url, { - method: "GET", - mode: "same-origin", - credentials: "omit", - cache: "no-store", - redirect: "follow", - }); - - const response = await fetch(request); - return !response.ok; - } catch { - return false; - } - } - - /** - * 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 getWellKnownChangePasswordUrl(urlOrigin: string): Promise { - try { - const url = new URL("./.well-known/change-password", urlOrigin); - - const request = new Request(url, { - method: "GET", - mode: "same-origin", - credentials: "omit", - cache: "no-store", - redirect: "follow", - }); - - const response = await fetch(request); - - return response.ok ? url.toString() : null; - } catch { - return null; - } - } -} 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 a0b5646c5a9..1c9c7c44405 100644 --- a/libs/vault/src/services/default-change-login-password.service.ts +++ b/libs/vault/src/services/default-change-login-password.service.ts @@ -1,19 +1,38 @@ -import { Injectable } from "@angular/core"; +import { Inject, Injectable } from "@angular/core"; -import { ApiService } from "@bitwarden/common/abstractions/api.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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service"; +interface PartialApiService { + nativeFetch(request: Request): Promise; +} +interface PartialPlatformUtilsService { + getClientType(): string; +} @Injectable() export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordService { + private apiService: PartialApiService; + private platformUtilsService: PartialPlatformUtilsService; + + // Constructor for Angular DI constructor( - private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, - ) {} + @Inject("IApiService") apiService: PartialApiService, + @Inject("IPlatformUtilsService") platformUtilsService: PartialPlatformUtilsService, + ) { + this.apiService = apiService; + this.platformUtilsService = platformUtilsService; + } + + // Static factory method for standalone creation + static create(apiService: PartialApiService, platformUtilsService: PartialPlatformUtilsService) { + const instance = new DefaultChangeLoginPasswordService(null!, null!); + instance.apiService = apiService; + instance.platformUtilsService = platformUtilsService; + return instance; + } /** * @inheritDoc