diff --git a/apps/web/src/app/settings/two-factor-authenticator.component.ts b/apps/web/src/app/settings/two-factor-authenticator.component.ts index 652ee24a144..90825f98bec 100644 --- a/apps/web/src/app/settings/two-factor-authenticator.component.ts +++ b/apps/web/src/app/settings/two-factor-authenticator.component.ts @@ -7,6 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti import { StateService } from "@bitwarden/common/abstractions/state.service"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { TwoFactorProviderType } from "@bitwarden/common/enums/twoFactorProviderType"; +import { Utils } from "@bitwarden/common/misc/utils"; import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/models/request/update-two-factor-authenticator.request"; import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/models/response/two-factor-authenticator.response"; import { AuthResponse } from "@bitwarden/common/types/authResponse"; @@ -99,7 +100,7 @@ export class TwoFactorAuthenticatorComponent element: document.getElementById("qr"), value: "otpauth://totp/Bitwarden:" + - encodeURIComponent(email) + + Utils.encodeRFC3986URIComponent(email) + "?secret=" + encodeURIComponent(this.key) + "&issuer=Bitwarden", diff --git a/libs/common/spec/misc/utils.spec.ts b/libs/common/spec/misc/utils.spec.ts index fb57c33fff1..e2fb27a1ebd 100644 --- a/libs/common/spec/misc/utils.spec.ts +++ b/libs/common/spec/misc/utils.spec.ts @@ -309,4 +309,21 @@ describe("Utils Service", () => { expect(Utils.recordToMap(map as any)).toEqual(map); }); }); + + describe("encodeRFC3986URIComponent", () => { + it("returns input string with expected encoded chars", () => { + expect(Utils.encodeRFC3986URIComponent("test'user@example.com")).toBe( + "test%27user%40example.com" + ); + expect(Utils.encodeRFC3986URIComponent("(test)user@example.com")).toBe( + "%28test%29user%40example.com" + ); + expect(Utils.encodeRFC3986URIComponent("testuser!@example.com")).toBe( + "testuser%21%40example.com" + ); + expect(Utils.encodeRFC3986URIComponent("Test*User@example.com")).toBe( + "Test%2AUser%40example.com" + ); + }); + }); }); diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index 48deeefcd3f..821f02d0a0c 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -486,6 +486,18 @@ export class Utils { return Object.assign(destination, source) as unknown as Merge; } + /** + * encodeURIComponent escapes all characters except the following: + * alphabetic, decimal digits, - _ . ! ~ * ' ( ) + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986 + */ + static encodeRFC3986URIComponent(str: string): string { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}` + ); + } + private static isMobile(win: Window) { let mobile = false; ((a) => { diff --git a/libs/common/src/services/policy/policy-api.service.ts b/libs/common/src/services/policy/policy-api.service.ts index 98848692dda..5b15f0199e5 100644 --- a/libs/common/src/services/policy/policy-api.service.ts +++ b/libs/common/src/services/policy/policy-api.service.ts @@ -5,6 +5,7 @@ import { PolicyApiServiceAbstraction } from "../../abstractions/policy/policy-ap import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction"; import { StateService } from "../../abstractions/state.service"; import { PolicyType } from "../../enums/policyType"; +import { Utils } from "../../misc/utils"; import { PolicyData } from "../../models/data/policy.data"; import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; import { PolicyRequest } from "../../models/request/policy.request"; @@ -54,7 +55,7 @@ export class PolicyApiService implements PolicyApiServiceAbstraction { "token=" + encodeURIComponent(token) + "&email=" + - encodeURIComponent(email) + + Utils.encodeRFC3986URIComponent(email) + "&organizationUserId=" + organizationUserId, null,