From bd36e9ca406c1eb50f2f4ad3ed917a80bec6a84c Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Fri, 6 Feb 2026 10:53:52 -0500 Subject: [PATCH] fix(redirect): [PM-30810] Https Redirection for Cloud Users - Looking at payload body and removed header override solution. --- .../DuoUniversalTokenService.cs | 11 +- .../Services/DuoUniversalTokenServiceTests.cs | 109 ++++++++++++++++-- 2 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs index ac92c71932..bd5a8c7fa0 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs @@ -181,10 +181,13 @@ public class DuoUniversalTokenService( return null; } - // Querystring has precedence over header for manual local testing - var overrideFromQuery = httpContext.Request.Query["deeplinkScheme"].FirstOrDefault(); - var overrideFromHeader = httpContext.Request.Headers["Bitwarden-Deeplink-Scheme"].FirstOrDefault(); - var candidate = (overrideFromQuery ?? overrideFromHeader)?.Trim(); + // Form data (POST body) has precedence, then header as fallback + string overrideFromForm = null; + if (httpContext.Request.HasFormContentType) + { + overrideFromForm = httpContext.Request.Form["deeplinkScheme"].FirstOrDefault(); + } + var candidate = overrideFromForm?.Trim(); // Allow only the two supported values return Enum.TryParse(candidate, ignoreCase: true, out var scheme) ? scheme : null; diff --git a/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs b/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs index bb374a4405..688482d4a4 100644 --- a/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs +++ b/test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs @@ -103,16 +103,79 @@ public class DuoUniversalTokenServiceTests Assert.Equal(result, expectedResponse); } - [Theory] - [BitAutoData("vault.bitwarden.com")] // Cloud US - [BitAutoData("vault.bitwarden.eu")] // Cloud EU - public void BuildDuoTwoFactorRedirectUri_MobileClient_CloudHost_ReturnsHttpsScheme( - string requestHost) + [Fact] + public void BuildDuoTwoFactorRedirectUri_MobileClient_NoOverride_DefaultsToBitwardenScheme() { // Arrange var httpContext = new DefaultHttpContext(); httpContext.Request.Headers["Bitwarden-Client-Name"] = "mobile"; - httpContext.Request.Host = new HostString(requestHost); + httpContext.Request.Host = new HostString("vault.bitwarden.com"); + + var currentContext = Substitute.For(); + currentContext.HttpContext.Returns(httpContext); + + var globalSettings = new CoreSettings.GlobalSettings + { + BaseServiceUri = new CoreSettings.GlobalSettings.BaseServiceUriSettings(new CoreSettings.GlobalSettings()) { Vault = "https://vault.bitwarden.com" } + }; + + var sut = new DuoUniversalTokenService(currentContext, globalSettings); + + // Act + var result = InvokeBuildDuoTwoFactorRedirectUri(sut); + + // Assert + Assert.Contains("client=mobile", result); + Assert.Contains("deeplinkScheme=bitwarden", result); + } + + [Theory] + [BitAutoData("https")] + [BitAutoData("bitwarden")] + public void BuildDuoTwoFactorRedirectUri_MobileClient_WithFormOverride_UsesOverrideScheme( + string schemeOverride) + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Bitwarden-Client-Name"] = "mobile"; + httpContext.Request.ContentType = "application/x-www-form-urlencoded"; + httpContext.Request.Form = new FormCollection(new Dictionary + { + { "deeplinkScheme", schemeOverride } + }); + httpContext.Request.Host = new HostString("vault.bitwarden.com"); + + var currentContext = Substitute.For(); + currentContext.HttpContext.Returns(httpContext); + + var globalSettings = new CoreSettings.GlobalSettings + { + BaseServiceUri = new CoreSettings.GlobalSettings.BaseServiceUriSettings(new CoreSettings.GlobalSettings()) { Vault = "https://vault.bitwarden.com" } + }; + + var sut = new DuoUniversalTokenService(currentContext, globalSettings); + + // Act + var result = InvokeBuildDuoTwoFactorRedirectUri(sut); + + // Assert + Assert.Contains("client=mobile", result); + Assert.Contains($"deeplinkScheme={schemeOverride.ToLowerInvariant()}", result); + } + + [Fact] + public void BuildDuoTwoFactorRedirectUri_MobileClient_FormOverrideTakesPrecedenceOverHeader() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Bitwarden-Client-Name"] = "mobile"; + httpContext.Request.Headers["Bitwarden-Deeplink-Scheme"] = "bitwarden"; + httpContext.Request.ContentType = "application/x-www-form-urlencoded"; + httpContext.Request.Form = new FormCollection(new Dictionary + { + { "deeplinkScheme", "https" } + }); + httpContext.Request.Host = new HostString("vault.bitwarden.com"); var currentContext = Substitute.For(); currentContext.HttpContext.Returns(httpContext); @@ -130,7 +193,37 @@ public class DuoUniversalTokenServiceTests // Assert Assert.Contains("client=mobile", result); Assert.Contains("deeplinkScheme=https", result); - Assert.StartsWith("https://vault.bitwarden.com/duo-redirect-connector.html", result); + } + + [Theory] + [BitAutoData("invalid")] + [BitAutoData("unknown")] + [BitAutoData("")] + public void BuildDuoTwoFactorRedirectUri_MobileClient_InvalidOverride_DefaultsToBitwardenScheme( + string invalidScheme) + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Bitwarden-Client-Name"] = "mobile"; + httpContext.Request.Headers["Bitwarden-Deeplink-Scheme"] = invalidScheme; + httpContext.Request.Host = new HostString("vault.bitwarden.com"); + + var currentContext = Substitute.For(); + currentContext.HttpContext.Returns(httpContext); + + var globalSettings = new CoreSettings.GlobalSettings + { + BaseServiceUri = new CoreSettings.GlobalSettings.BaseServiceUriSettings(new CoreSettings.GlobalSettings()) { Vault = "https://vault.bitwarden.com" } + }; + + var sut = new DuoUniversalTokenService(currentContext, globalSettings); + + // Act + var result = InvokeBuildDuoTwoFactorRedirectUri(sut); + + // Assert + Assert.Contains("client=mobile", result); + Assert.Contains("deeplinkScheme=bitwarden", result); } [Theory] @@ -300,6 +393,6 @@ public class DuoUniversalTokenServiceTests // Assert Assert.Contains("client=mobile", result); - Assert.Contains("deeplinkScheme=https", result); + Assert.Contains("deeplinkScheme=bitwarden", result); } }