diff --git a/api/Controllers/AuthController.cs b/api/Controllers/AuthController.cs index 21e413d9..5c54f9a1 100644 --- a/api/Controllers/AuthController.cs +++ b/api/Controllers/AuthController.cs @@ -99,7 +99,12 @@ await Db.RequestFileAccess.AddAsync(new RequestFileAccess return Ok(new { - Url = $"{XForwardedForHelper.BuildUrlString(forwardedHost, forwardedPort, baseUrl)}civil-file/{request.FileId}?fromA2A=true" + Url = XForwardedForHelper.BuildUrlString( + forwardedHost, + forwardedPort, + baseUrl, + $"civil-file/{request.FileId}", + "fromA2A=true") }); } diff --git a/api/Helpers/XForwardedForHelper.cs b/api/Helpers/XForwardedForHelper.cs index 11783946..8ff4a0bb 100644 --- a/api/Helpers/XForwardedForHelper.cs +++ b/api/Helpers/XForwardedForHelper.cs @@ -1,11 +1,38 @@ -namespace Scv.Api.Helpers +using System; + +namespace Scv.Api.Helpers { public static class XForwardedForHelper { - public static string BuildUrlString(string forwardedHost, string forwardedPort, string baseUrl) + public static string BuildUrlString(string forwardedHost, string forwardedPort, string baseUrl, string remainingPath = "", string query = "") { - var portComponent = string.IsNullOrEmpty(forwardedPort) || forwardedPort == "80" || forwardedPort == "443" ? "" : $":{forwardedPort}"; - return $"https://{forwardedHost}{portComponent}{baseUrl}"; + var sanitizedPath = baseUrl; + if (!string.IsNullOrEmpty(remainingPath)) + { + sanitizedPath = string.Format("{0}/{1}", baseUrl.TrimEnd('/'), remainingPath.TrimStart('/')); + } + + var uriBuilder = new UriBuilder + { + Scheme = "https", + Host = forwardedHost, + Path = sanitizedPath, + Query = query + }; + + var portComponent = + string.IsNullOrEmpty(forwardedPort) || forwardedPort == "80" || forwardedPort == "443" + ? "" + : $":{forwardedPort}"; + + if (!string.IsNullOrEmpty(portComponent)) + { + int port; + int.TryParse(forwardedPort, out port); + uriBuilder.Port = port; + } + + return uriBuilder.Uri.AbsoluteUri; } } } diff --git a/api/Infrastructure/Authentication/AuthenticationServiceCollectionExtension.cs b/api/Infrastructure/Authentication/AuthenticationServiceCollectionExtension.cs index 269114df..3663b03f 100644 --- a/api/Infrastructure/Authentication/AuthenticationServiceCollectionExtension.cs +++ b/api/Infrastructure/Authentication/AuthenticationServiceCollectionExtension.cs @@ -129,7 +129,7 @@ await cookieCtx.HttpContext.SignOutAsync(CookieAuthenticationDefaults var loggerFactory = context.HttpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("OnTokenValidated"); - logger.LogInformation($"OpenIdConnect UserId - { context.Principal.UserId() } - logged in."); + logger.LogInformation($"OpenIdConnect UserId - {context.Principal.UserId()} - logged in."); //Cleanup keycloak claims, that are unused. foreach (var claim in identity.Claims.WhereToList(c => @@ -152,13 +152,13 @@ await cookieCtx.HttpContext.SignOutAsync(CookieAuthenticationDefaults if (fileAccess != null && !string.IsNullOrEmpty(fileAccess.PartId) && !string.IsNullOrEmpty(fileAccess.AgencyId)) { - logger.LogInformation($"UserId - { context.Principal.UserId() } - Using credentials passed in from A2A."); + logger.LogInformation($"UserId - {context.Principal.UserId()} - Using credentials passed in from A2A."); var aesGcmEncryption = context.HttpContext.RequestServices.GetRequiredService(); partId = aesGcmEncryption.Decrypt(fileAccess.PartId); agencyId = aesGcmEncryption.Decrypt(fileAccess.AgencyId); applicationCode = "A2A"; } - } + } else if (context.Principal.IsIdirUser() && context.Principal.Groups().Contains("court-viewer-supreme")) { isSupremeUser = true; @@ -197,8 +197,11 @@ await cookieCtx.HttpContext.SignOutAsync(CookieAuthenticationDefaults var forwardedHost = context.HttpContext.Request.Headers["X-Forwarded-Host"]; var forwardedPort = context.HttpContext.Request.Headers["X-Forwarded-Port"]; var baseUrl = context.HttpContext.Request.Headers["X-Base-Href"]; - context.ProtocolMessage.RedirectUri = - $"{XForwardedForHelper.BuildUrlString(forwardedHost, forwardedPort, baseUrl)}{options.CallbackPath}"; + context.ProtocolMessage.RedirectUri = XForwardedForHelper.BuildUrlString( + forwardedHost, + forwardedPort, + baseUrl, + options.CallbackPath); } return Task.CompletedTask; } diff --git a/tests/api/Helpers/XForwardedForHelperTests.cs b/tests/api/Helpers/XForwardedForHelperTests.cs new file mode 100644 index 00000000..28fee2a4 --- /dev/null +++ b/tests/api/Helpers/XForwardedForHelperTests.cs @@ -0,0 +1,133 @@ +using System.Net; +using System.Collections.Generic; +using System; +using Bogus; +using Scv.Api.Helpers; +using Xunit; + +namespace tests.api.Helpers +{ + public class XForwardedForHelperTests + { + [Fact] + public void BuildUrlString_ShouldRemoveDoubleSlashesInUrlPath() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var path1 = WebUtility.UrlEncode(faker.Random.Word()); + var path2 = WebUtility.UrlEncode(faker.Random.Word()); + var port = 8080; + var expected = $"https://{host}:{port}/{path1}/{path2}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", $"{path1}///", $"//{path2}"); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuildUrlString_ShouldRemoveDoubleSlashesInUrlPathWithManyForwardSlash() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var path1 = WebUtility.UrlEncode(faker.Random.Word()); + var path2 = WebUtility.UrlEncode(faker.Random.Word()); + var port = 80; + var expected = $"https://{host}/{path1}/{path2}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", $"{path1}//////////////", $"///////////////{path2}"); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuildUrlString_ShouldExcludeWhenPortIs443() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var path1 = WebUtility.UrlEncode(faker.Random.Word()); + var path2 = WebUtility.UrlEncode(faker.Random.Word()); + var port = 443; + var expected = $"https://{host}/{path1}/{path2}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", path1, path2); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuildUrlString_ShouldExcludeWhenPortIs80() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var path1 = WebUtility.UrlEncode(faker.Random.Word()); + var path2 = WebUtility.UrlEncode(faker.Random.Word()); + var port = 80; + var expected = $"https://{host}/{path1}/{path2}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", path1, path2); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuildUrlString_ShouldReturnCorrectURLWhenNoOtherUrlPath() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var path1 = WebUtility.UrlEncode(faker.Random.Word()); + var port = 80; + var expected = $"https://{host}/{path1}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", path1); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuildUrlString_ShouldReturnCorrectURLWithQueryParams() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var path1 = WebUtility.UrlEncode(faker.Random.Word()); + var path2 = WebUtility.UrlEncode(faker.Random.Word()); + var port = 80; + string param1 = WebUtility.UrlEncode($"{faker.Lorem.Word()}={faker.Random.Number(1, 100)}"); + string param2 = WebUtility.UrlEncode($"{faker.Lorem.Word()}={faker.Internet.UserName()}"); + string param3 = WebUtility.UrlEncode($"{faker.Lorem.Word()}={faker.Random.Bool()}"); + + var expected = $"https://{host}/{path1}/{path2}?{param1}&{param2}&{param3}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", path1, path2, $"{param1}&{param2}&{param3}"); + + Assert.Equal(expected, result); + } + + [Fact] + public void BuildUrlString_ShouldReturnCorrectURLRandomPaths() + { + var faker = new Faker(); + var host = faker.Internet.DomainName(); + var basePath = WebUtility.UrlEncode(faker.Random.Word()); + var remainingPathCount = faker.Random.Number(1, 5); + var paths = new List(); + var port = 80; + + string param1 = WebUtility.UrlEncode($"{faker.Lorem.Word()}={faker.Random.Number(1, 100)}"); + string param2 = WebUtility.UrlEncode($"{faker.Lorem.Word()}={faker.Internet.UserName()}"); + string param3 = WebUtility.UrlEncode($"{faker.Lorem.Word()}={faker.Random.Bool()}"); + + for (int i = 0; i < remainingPathCount; i++) + { + paths.Add(WebUtility.UrlEncode(faker.Random.Word())); + } + + var expected = $"https://{host}/{basePath}/{string.Join("/", paths)}?{param1}&{param2}&{param3}"; + + var result = XForwardedForHelper.BuildUrlString(host, $"{port}", basePath, string.Join("/", paths), $"{param1}&{param2}&{param3}"); + + Console.WriteLine(expected); + + Assert.Equal(expected, result); + } + } +} \ No newline at end of file diff --git a/tests/tests.csproj b/tests/tests.csproj index b33a42f7..d69adb9d 100644 --- a/tests/tests.csproj +++ b/tests/tests.csproj @@ -9,6 +9,7 @@ +