From 4a3bdbd33f23a4eaeb313ca7849ece320066f0f4 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 27 Oct 2023 10:55:52 +0100 Subject: [PATCH 1/4] Add customerId to gesture path This isn't currently used in code execution but keeps paths consistent and gives scope to use later if required --- .../Integration/AccessServiceTests.cs | 10 +++++----- .../IIIFAuth2.API/Features/Access/AccessController.cs | 2 +- .../Features/Access/Views/SignificantGesture.cshtml | 2 +- .../Auth/Models/SignificantGestureModel.cs | 2 ++ .../ClickthroughRoleProviderHandler.cs | 1 + 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/IIIFAuth2/IIIFAuth2.API.Tests/Integration/AccessServiceTests.cs b/src/IIIFAuth2/IIIFAuth2.API.Tests/Integration/AccessServiceTests.cs index d48a8c6..65ef5f8 100644 --- a/src/IIIFAuth2/IIIFAuth2.API.Tests/Integration/AccessServiceTests.cs +++ b/src/IIIFAuth2/IIIFAuth2.API.Tests/Integration/AccessServiceTests.cs @@ -233,7 +233,7 @@ public async Task AccessService_Clickthrough_RendersWindowClose_IfSameHost() public async Task SignificantGesture_Returns400_IfNoSingleUseToken() { // Arrange - const string path = "/access/gesture"; + const string path = "/access/99/gesture"; // Act var formContent = new FormUrlEncodedContent(new[] @@ -250,7 +250,7 @@ public async Task SignificantGesture_Returns400_IfNoSingleUseToken() public async Task SignificantGesture_RendersWindowClose_WithError_IfTokenExpired() { // Arrange - const string path = "/access/gesture"; + const string path = "/access/99/gesture"; var expiredToken = ExpiringToken.GenerateNewToken(DateTime.UtcNow.AddHours(-1)); // Act @@ -270,7 +270,7 @@ public async Task SignificantGesture_RendersWindowClose_WithError_IfTokenExpired public async Task SignificantGesture_RendersWindowClose_WithError_IfTokenValidButNotInDatabase() { // Arrange - const string path = "/access/gesture"; + const string path = "/access/99/gesture"; var expiredToken = ExpiringToken.GenerateNewToken(DateTime.UtcNow.AddHours(1)); // Act @@ -290,7 +290,7 @@ public async Task SignificantGesture_RendersWindowClose_WithError_IfTokenValidBu public async Task SignificantGesture_RendersWindowClose_WithError_IfTokenValidButUsed() { // Arrange - const string path = "/access/gesture"; + const string path = "/access/99/gesture"; var validToken = ExpiringToken.GenerateNewToken(DateTime.UtcNow.AddHours(1)); await dbContext.RoleProvisionTokens.AddAsync(CreateToken(validToken, true, Array.Empty())); await dbContext.SaveChangesAsync(); @@ -312,7 +312,7 @@ public async Task SignificantGesture_RendersWindowClose_WithError_IfTokenValidBu public async Task SignificantGesture_CreatesSession_AndSetsCookie_AndMarksTokenAsUsed() { // Arrange - const string path = "/access/gesture"; + const string path = "/access/99/gesture"; var validToken = ExpiringToken.GenerateNewToken(DateTime.UtcNow.AddHours(1)); var roles = new string[] { DatabaseFixture.ClickthroughRoleUri }; var tokenEntity = await dbContext.RoleProvisionTokens.AddAsync(CreateToken(validToken, false, roles)); diff --git a/src/IIIFAuth2/IIIFAuth2.API/Features/Access/AccessController.cs b/src/IIIFAuth2/IIIFAuth2.API/Features/Access/AccessController.cs index 2eaa54b..5073332 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Features/Access/AccessController.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Features/Access/AccessController.cs @@ -57,7 +57,7 @@ public async Task AccessService( /// This is required for us to issue a cookie to user. /// [HttpPost] - [Route("gesture")] + [Route("{customerId}/gesture")] public async Task SignificantGesture( [FromForm] string singleUseToken, CancellationToken cancellationToken) diff --git a/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml b/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml index 6e0d5b9..e44b866 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml +++ b/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml @@ -8,7 +8,7 @@

@Model.SignificantGestureMessage

-@using (Html.BeginForm("SignificantGesture", "Access")) +@using (Html.BeginForm("SignificantGesture", "Access", new { customerId = Model.CustomerId})) { @Html.HiddenFor(m => m.SingleUseToken) diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs index dab3009..205b372 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs @@ -5,12 +5,14 @@ namespace IIIFAuth2.API.Infrastructure.Auth.Models; /// domain for a cookie to be issued, this captures a confirmation click to allow DLCS to issue a token that the browser /// will honour /// +/// CustomerId of asset being accessed /// Title of page /// Information message to display to user /// /// Single use correlation id, from this we can lookup CustomerId, AccessServiceName + Roles to grant to user. /// public record SignificantGestureModel( + int CustomerId, string SignificantGestureTitle, string SignificantGestureMessage, string SingleUseToken); \ No newline at end of file diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs index 19a12cd..baec005 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs @@ -80,6 +80,7 @@ private async Task GetSignificantGestureModel(int custo var expiringToken = await sessionManagementService.CreateRoleProvisionToken(customerId, roles, origin, cancellationToken); var gestureModel = new SignificantGestureModel( + customerId, configuration.GestureTitle ?? apiSettings.DefaultSignificantGestureTitle, configuration.GestureMessage ?? apiSettings.DefaultSignificantGestureMessage, expiringToken); From eba71ecd5c3a13ea419006aa1ffb7bd5c13b4b0d Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 27 Oct 2023 12:24:54 +0100 Subject: [PATCH 2/4] Allow sig-gesture path to differ by host --- docker-compose.local.yml | 2 +- .../Converters/AccessServiceConverterTests.cs | 7 ++-- .../Utils/HttpRequestXTests.cs | 20 +++++++++++ .../Access/Views/SignificantGesture.cshtml | 5 ++- .../Auth/Models/SignificantGestureModel.cs | 4 +-- .../ClickthroughRoleProviderHandler.cs | 7 +++- .../Infrastructure/Web/UrlPathProvider.cs | 35 ++++++++++++++++--- .../IIIFAuth2.API/Settings/ApiSettings.cs | 4 +++ .../IIIFAuth2.API/Utils/HttpRequestX.cs | 15 ++++---- 9 files changed, 80 insertions(+), 19 deletions(-) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index b46be1c..b76969c 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -9,7 +9,7 @@ services: image: postgres:14 hostname: postgres ports: - - "5433:5432" + - "5430:5432" volumes: - auth_postgres_data:/var/lib/postgresql/data - auth_postgres_data_backups:/backups diff --git a/src/IIIFAuth2/IIIFAuth2.API.Tests/Models/Converters/AccessServiceConverterTests.cs b/src/IIIFAuth2/IIIFAuth2.API.Tests/Models/Converters/AccessServiceConverterTests.cs index a742b97..8a84303 100644 --- a/src/IIIFAuth2/IIIFAuth2.API.Tests/Models/Converters/AccessServiceConverterTests.cs +++ b/src/IIIFAuth2/IIIFAuth2.API.Tests/Models/Converters/AccessServiceConverterTests.cs @@ -226,7 +226,10 @@ public Uri GetAccessServicePath(AccessService accessService) public Uri GetAccessServiceLogoutPath(AccessService accessService) => new($"http://test.example/access/{accessService.Name}/logout"); - public Uri GetAccessTokenServicePath(int customer) - => new($"http://test.example/token/{customer}"); + public Uri GetAccessTokenServicePath(int customerId) + => new($"http://test.example/token/{customerId}"); + + public Uri GetGesturePostbackRelativePath(int customerId) + => new($"/access/{customerId}/gesture", UriKind.Relative); } } \ No newline at end of file diff --git a/src/IIIFAuth2/IIIFAuth2.API.Tests/Utils/HttpRequestXTests.cs b/src/IIIFAuth2/IIIFAuth2.API.Tests/Utils/HttpRequestXTests.cs index 62d9f2d..78f2a24 100644 --- a/src/IIIFAuth2/IIIFAuth2.API.Tests/Utils/HttpRequestXTests.cs +++ b/src/IIIFAuth2/IIIFAuth2.API.Tests/Utils/HttpRequestXTests.cs @@ -121,6 +121,26 @@ public void GetDisplayUrl_WithPathBase_ReturnsFullUrl_WithoutQueryParam_WhenCall // Assert result.Should().Be(expected); } + + [Fact] + public void GetDisplayUrl_WithPathBase_ReturnsFullUrl_WithoutQueryParam_WhenCalledWithDoNotIncludeHost() + { + // Arrange + var httpRequest = new DefaultHttpContext().Request; + httpRequest.Path = new PathString("/anything"); + httpRequest.QueryString = new QueryString("?foo=bar"); + httpRequest.Host = new HostString("test.example"); + httpRequest.PathBase = new PathString("/v2"); + httpRequest.Scheme = "https"; + + const string expected = "/v2/test"; + + // Act + var result = httpRequest.GetDisplayUrl("/test", includeQueryParams: false, includeHost: false); + + // Assert + result.Should().Be(expected); + } [Theory] [InlineData("https://test.example")] diff --git a/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml b/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml index e44b866..8455cf5 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml +++ b/src/IIIFAuth2/IIIFAuth2.API/Features/Access/Views/SignificantGesture.cshtml @@ -8,10 +8,9 @@

@Model.SignificantGestureMessage

-@using (Html.BeginForm("SignificantGesture", "Access", new { customerId = Model.CustomerId})) -{ +
@Html.HiddenFor(m => m.SingleUseToken) -} +
\ No newline at end of file diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs index 205b372..224c74e 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/Models/SignificantGestureModel.cs @@ -5,14 +5,14 @@ namespace IIIFAuth2.API.Infrastructure.Auth.Models; /// domain for a cookie to be issued, this captures a confirmation click to allow DLCS to issue a token that the browser /// will honour /// -/// CustomerId of asset being accessed +/// Relative Uri to post message back to. Can differ depending on hostname /// Title of page /// Information message to display to user /// /// Single use correlation id, from this we can lookup CustomerId, AccessServiceName + Roles to grant to user. /// public record SignificantGestureModel( - int CustomerId, + Uri PostbackUri, string SignificantGestureTitle, string SignificantGestureMessage, string SingleUseToken); \ No newline at end of file diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs index baec005..103c1f5 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Auth/RoleProvisioning/ClickthroughRoleProviderHandler.cs @@ -1,6 +1,7 @@ using IIIFAuth2.API.Data; using IIIFAuth2.API.Data.Entities; using IIIFAuth2.API.Infrastructure.Auth.Models; +using IIIFAuth2.API.Infrastructure.Web; using IIIFAuth2.API.Models.Domain; using IIIFAuth2.API.Settings; using Microsoft.Extensions.Options; @@ -16,16 +17,19 @@ public class ClickthroughRoleProviderHandler : IRoleProviderHandler private readonly SessionManagementService sessionManagementService; private readonly ILogger logger; private readonly ApiSettings apiSettings; + private readonly IUrlPathProvider urlPathProvider; public ClickthroughRoleProviderHandler( AuthServicesContext dbContext, SessionManagementService sessionManagementService, + IUrlPathProvider urlPathProvider, IOptions apiOptions, ILogger logger) { this.dbContext = dbContext; this.sessionManagementService = sessionManagementService; this.logger = logger; + this.urlPathProvider = urlPathProvider; apiSettings = apiOptions.Value; } @@ -79,8 +83,9 @@ private async Task GetSignificantGestureModel(int custo { var expiringToken = await sessionManagementService.CreateRoleProvisionToken(customerId, roles, origin, cancellationToken); + var relativePath = urlPathProvider.GetGesturePostbackRelativePath(customerId); var gestureModel = new SignificantGestureModel( - customerId, + relativePath, configuration.GestureTitle ?? apiSettings.DefaultSignificantGestureTitle, configuration.GestureMessage ?? apiSettings.DefaultSignificantGestureMessage, expiringToken); diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs index 999d851..469132e 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs @@ -26,7 +26,14 @@ public interface IUrlPathProvider /// /// Get the path for AccessService /// - Uri GetAccessTokenServicePath(int customer); + Uri GetAccessTokenServicePath(int customerId); + + /// + /// Get the relative Uri for posting back + /// + /// + /// + Uri GetGesturePostbackRelativePath(int customerId); } public class UrlPathProvider : IUrlPathProvider @@ -80,10 +87,10 @@ public Uri GetAccessServiceLogoutPath(AccessService accessService) } /// - public Uri GetAccessTokenServicePath(int customer) + public Uri GetAccessTokenServicePath(int customerId) { var baseUrl = GetCurrentBaseUrl(); - var path = $"/auth/v2/access/{customer}/token"; + var path = $"/auth/v2/access/{customerId}/token"; var builder = new UriBuilder(baseUrl) { Path = path @@ -92,6 +99,26 @@ public Uri GetAccessTokenServicePath(int customer) return builder.Uri; } + /// + public Uri GetGesturePostbackRelativePath(int customerId) + { + var request = httpContextAccessor.SafeHttpContext().Request; + var host = request.Host.Value; + + var template = GetPopulatedTemplate(host, customerId); + return new Uri(template, UriKind.Relative); + } + + private string GetPopulatedTemplate(string host, int customerId) + { + const string defaultPathTemplate = "/access/{customerId}/gesture"; + var template = apiSettings.Auth.GesturePathTemplateForDomain.TryGetValue(host, out var pathTemplate) + ? pathTemplate + : defaultPathTemplate; + + return template.Replace("{customerId}", customerId.ToString()); + } + private string GetCurrentBaseUrl() => - httpContextAccessor.HttpContext?.Request.GetDisplayUrl(null, false) ?? string.Empty; + httpContextAccessor.SafeHttpContext().Request.GetDisplayUrl(null, false); } diff --git a/src/IIIFAuth2/IIIFAuth2.API/Settings/ApiSettings.cs b/src/IIIFAuth2/IIIFAuth2.API/Settings/ApiSettings.cs index 6266898..9fc2b52 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Settings/ApiSettings.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Settings/ApiSettings.cs @@ -10,6 +10,8 @@ public class ApiSettings /// Used to generate Probe request paths public Uri OrchestratorRoot { get; set; } = null!; + public AuthSettings Auth { get; set; } = new(); + /// /// Fallback title for Significant Gesture view, if none specified by RoleProvider /// @@ -39,4 +41,6 @@ public class AuthSettings /// /// This avoids constant churn in db public int RefreshThreshold { get; set; } = 120; + + public Dictionary GesturePathTemplateForDomain { get; set; } = new(); } \ No newline at end of file diff --git a/src/IIIFAuth2/IIIFAuth2.API/Utils/HttpRequestX.cs b/src/IIIFAuth2/IIIFAuth2.API/Utils/HttpRequestX.cs index 9cd9610..dbab582 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Utils/HttpRequestX.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Utils/HttpRequestX.cs @@ -5,21 +5,24 @@ namespace IIIFAuth2.API.Utils; public static class HttpRequestX { private const string SchemeDelimiter = "://"; - + /// /// Generate a full display URL, deriving values from specified HttpRequest /// /// HttpRequest to generate display URL for /// Path to append to URL /// If true, query params are included in path. Else they are omitted + /// If true, host and scheme are included in path. Else it is omitted /// Full URL, including scheme, host, pathBase, path and queryString /// /// based on Microsoft.AspNetCore.Http.Extensions.UriHelper.GetDisplayUrl(this HttpRequest request) /// - public static string GetDisplayUrl(this HttpRequest request, string? path = null, bool includeQueryParams = true) + public static string GetDisplayUrl(this HttpRequest request, string? path = null, bool includeQueryParams = true, + bool includeHost = true) { - var host = request.Host.Value ?? string.Empty; - var scheme = request.Scheme ?? string.Empty; + var host = includeHost ? request.Host.Value : string.Empty; + var scheme = includeHost ? request.Scheme : string.Empty; + var schemeDelimiterToUse = includeHost ? SchemeDelimiter : string.Empty; var pathBase = request.PathBase.Value ?? string.Empty; var queryString = includeQueryParams ? request.QueryString.Value ?? string.Empty @@ -27,12 +30,12 @@ public static string GetDisplayUrl(this HttpRequest request, string? path = null var pathElement = path ?? string.Empty; // PERF: Calculate string length to allocate correct buffer size for StringBuilder. - var length = scheme.Length + SchemeDelimiter.Length + host.Length + var length = scheme.Length + schemeDelimiterToUse.Length + host.Length + pathBase.Length + pathElement.Length + queryString.Length; return new StringBuilder(length) .Append(scheme) - .Append(SchemeDelimiter) + .Append(schemeDelimiterToUse) .Append(host) .Append(pathBase) .Append(path) From 1612c1b043e4a7cde59a4c7d45b111362ce52502 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 27 Oct 2023 12:41:57 +0100 Subject: [PATCH 3/4] Better handling of gesture default path --- .../Web/UrlPathProviderTests.cs | 84 +++++++++++++++++++ .../Infrastructure/Web/UrlPathProvider.cs | 16 +++- 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs diff --git a/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs b/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs new file mode 100644 index 0000000..bb74ae3 --- /dev/null +++ b/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs @@ -0,0 +1,84 @@ +using FakeItEasy; +using IIIFAuth2.API.Infrastructure.Web; +using IIIFAuth2.API.Settings; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace IIIFAuth2.API.Tests.Infrastructure.Web; + +public class UrlPathProviderTests +{ + private const string CurrentHost = "dlcs.test.example"; + private const string OtherHost = "dlcs.test.other"; + + [Fact] + public void GetGesturePostbackRelativePath_HandlesNoConfiguredDefault() + { + // Arrange + var gestureTemplates = new Dictionary + { + [OtherHost] = "/access/specific-host" + }; + var sut = GetSut(CurrentHost, gestureTemplates); + + // Act + var result = sut.GetGesturePostbackRelativePath(123); + + // Asset + result.IsAbsoluteUri.Should().BeFalse(); + result.ToString().Should().Be("/access/123/gesture"); + } + + [Fact] + public void GetGesturePostbackRelativePath_UsesConfiguredDefault() + { + // Arrange + var gestureTemplates = new Dictionary + { + ["Default"] = "/access/other", + [OtherHost] = "/access/specific-host" + }; + var sut = GetSut(CurrentHost, gestureTemplates); + + // Act + var result = sut.GetGesturePostbackRelativePath(123); + + // Asset + result.IsAbsoluteUri.Should().BeFalse(); + result.ToString().Should().Be("/access/other"); + } + + [Fact] + public void GetGesturePostbackRelativePath_UsesSpecifiedHost_IfFound() + { + // Arrange + var gestureTemplates = new Dictionary + { + ["Default"] = "/access/other", + [CurrentHost] = "/{customerId}/access/gesture" + }; + var sut = GetSut(CurrentHost, gestureTemplates); + + // Act + var result = sut.GetGesturePostbackRelativePath(123); + + // Asset + result.IsAbsoluteUri.Should().BeFalse(); + result.ToString().Should().Be("/123/access/gesture"); + } + + private UrlPathProvider GetSut(string host, Dictionary gestureTemplates) + { + var context = new DefaultHttpContext(); + var request = context.Request; + request.Host = new HostString(host); + request.Scheme = "https"; + var contextAccessor = A.Fake(); + A.CallTo(() => contextAccessor.HttpContext).Returns(context); + + var authSettings = new AuthSettings { GesturePathTemplateForDomain = gestureTemplates }; + var apiSettings = Options.Create(new ApiSettings { Auth = authSettings }); + + return new UrlPathProvider(contextAccessor, apiSettings); + } +} \ No newline at end of file diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs index 469132e..9ef3faf 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs @@ -110,13 +110,21 @@ public Uri GetGesturePostbackRelativePath(int customerId) } private string GetPopulatedTemplate(string host, int customerId) + { + var template = GetTemplate(host); + return template.Replace("{customerId}", customerId.ToString()); + } + + private string GetTemplate(string host) { const string defaultPathTemplate = "/access/{customerId}/gesture"; - var template = apiSettings.Auth.GesturePathTemplateForDomain.TryGetValue(host, out var pathTemplate) - ? pathTemplate - : defaultPathTemplate; + const string defaultKey = "Default"; - return template.Replace("{customerId}", customerId.ToString()); + var pathTemplates = apiSettings.Auth.GesturePathTemplateForDomain; + + if (pathTemplates.TryGetValue(host, out var hostTemplate)) return hostTemplate; + if (pathTemplates.TryGetValue(defaultKey, out var pathTemplate)) return pathTemplate; + return defaultPathTemplate; } private string GetCurrentBaseUrl() => From 40bc0f9d18491906a7bf2b248185573b3b00253f Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 27 Oct 2023 14:49:04 +0100 Subject: [PATCH 4/4] Use pathBase in fallback default, if present --- .../Web/UrlPathProviderTests.cs | 22 +++++++++++++++++-- .../Infrastructure/Web/UrlPathProvider.cs | 14 ++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs b/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs index bb74ae3..107e79e 100644 --- a/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs +++ b/src/IIIFAuth2/IIIFAuth2.API.Tests/Infrastructure/Web/UrlPathProviderTests.cs @@ -29,6 +29,24 @@ public void GetGesturePostbackRelativePath_HandlesNoConfiguredDefault() result.ToString().Should().Be("/access/123/gesture"); } + [Fact] + public void GetGesturePostbackRelativePath_HandlesNoConfiguredDefault_WithPathBase() + { + // Arrange + var gestureTemplates = new Dictionary + { + [OtherHost] = "/access/specific-host" + }; + var sut = GetSut(CurrentHost, gestureTemplates, "auth/v2/"); + + // Act + var result = sut.GetGesturePostbackRelativePath(123); + + // Asset + result.IsAbsoluteUri.Should().BeFalse(); + result.ToString().Should().Be("auth/v2/access/123/gesture"); + } + [Fact] public void GetGesturePostbackRelativePath_UsesConfiguredDefault() { @@ -67,7 +85,7 @@ public void GetGesturePostbackRelativePath_UsesSpecifiedHost_IfFound() result.ToString().Should().Be("/123/access/gesture"); } - private UrlPathProvider GetSut(string host, Dictionary gestureTemplates) + private UrlPathProvider GetSut(string host, Dictionary gestureTemplates, string? pathBase = null) { var context = new DefaultHttpContext(); var request = context.Request; @@ -77,7 +95,7 @@ private UrlPathProvider GetSut(string host, Dictionary gestureTe A.CallTo(() => contextAccessor.HttpContext).Returns(context); var authSettings = new AuthSettings { GesturePathTemplateForDomain = gestureTemplates }; - var apiSettings = Options.Create(new ApiSettings { Auth = authSettings }); + var apiSettings = Options.Create(new ApiSettings { Auth = authSettings, PathBase = pathBase }); return new UrlPathProvider(contextAccessor, apiSettings); } diff --git a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs index 9ef3faf..3cef712 100644 --- a/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs +++ b/src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs @@ -1,4 +1,5 @@ -using IIIFAuth2.API.Data.Entities; +using System.Text.RegularExpressions; +using IIIFAuth2.API.Data.Entities; using IIIFAuth2.API.Models.Domain; using IIIFAuth2.API.Settings; using IIIFAuth2.API.Utils; @@ -114,17 +115,22 @@ private string GetPopulatedTemplate(string host, int customerId) var template = GetTemplate(host); return template.Replace("{customerId}", customerId.ToString()); } - + private string GetTemplate(string host) { const string defaultPathTemplate = "/access/{customerId}/gesture"; const string defaultKey = "Default"; var pathTemplates = apiSettings.Auth.GesturePathTemplateForDomain; - + if (pathTemplates.TryGetValue(host, out var hostTemplate)) return hostTemplate; if (pathTemplates.TryGetValue(defaultKey, out var pathTemplate)) return pathTemplate; - return defaultPathTemplate; + if (apiSettings.PathBase.IsNullOrEmpty()) return defaultPathTemplate; + + // Replace any duplicate slashes after joining path elements + var candidate = $"{apiSettings.PathBase}/{defaultPathTemplate}"; + var duplicateSlashRegex = new Regex("(/)+", RegexOptions.Compiled); + return duplicateSlashRegex.Replace(candidate, "$1"); } private string GetCurrentBaseUrl() =>