Skip to content

Commit

Permalink
Merge pull request #40 from dlcs/feature/domain_aware_redirect
Browse files Browse the repository at this point in the history
Allow oauth2 `redirect_url` to differ per domain
  • Loading branch information
donaldgray authored Jan 25, 2024
2 parents 6812d15 + a2281ef commit 7f3d117
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 38 deletions.
23 changes: 13 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ docker compose -f docker-compose.local.yml up

The following appSetting configuration values are supported:

| Name | Description | Default |
| -------------------------------- | ------------------------------------------------------------ | ----------------------------------------------- |
| OrchestratorRoot | Base URI for Orchestrator, used to generate links | |
| DefaultSignificantGestureTitle | Fallback title to use on SignificantGesture.cshtml | `"Click to continue"` |
| DefaultSignificantGestureMessage | Fallback message to use on SignificantGesture.cshtml | `"You will now be redirected to DLCS to login"` |
| Auth__CookieNameFormat | Name of issued cookie, `{0}` value replaced with customer Id | `"dlcs-auth2-{0}` |
| Auth__SessionTtl | Default TTL for sessions + cookies (in seconds) | 600 |
| Auth__CookieDomains | An optional list of domains to issue cookies for | |
| Auth__UseCurrentDomainForCookie | Whether current domain is automatically added to auth token | `true` |

| Name | Description | Default |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| OrchestratorRoot | Base URI for Orchestrator, used to generate links | |
| DefaultSignificantGestureTitle | Fallback title to use on SignificantGesture.cshtml | `"Click to continue"` |
| DefaultSignificantGestureMessage | Fallback message to use on SignificantGesture.cshtml | `"You will now be redirected to DLCS to login"` |
| Auth__CookieNameFormat | Name of issued cookie, `{0}` value replaced with customer Id | `"dlcs-auth2-{0}` |
| Auth__SessionTtl | Default TTL for sessions + cookies (in seconds) | 600 |
| Auth__RefreshThreshold | UserSession expiry not refreshed if LastChecked within this number of secs | 120 |
| Auth__JwksTtl | How long to cache results of JWKS calls for, in secs | 600 |
| GesturePathTemplateForDomain | Dictionary that allows control of domain-specific significant gesture paths. `{customerId}` replaced. | |
| OAuthCallbackPathTemplateForDomain | Dictionary that allows control of domain-specific oauth2 callback paths. `{customerId}` + `{accessService}` replaced. | |

> A note on Dictionarys for domain-specific paths. A key of `"Default"` serves as fallback but isn't necessary if the default value matches the canonical DLCS path.
## Migrations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void GetGesturePostbackRelativePath_HandlesNoConfiguredDefault()
{
[OtherHost] = "/access/specific-host"
};
var sut = GetSut(CurrentHost, gestureTemplates);
var sut = GetSut(CurrentHost, settings => settings.GesturePathTemplateForDomain = gestureTemplates);

// Act
var result = sut.GetGesturePostbackRelativePath(123);
Expand All @@ -38,7 +38,7 @@ public void GetGesturePostbackRelativePath_HandlesNoConfiguredDefault_WithPathBa
{
[OtherHost] = "/access/specific-host"
};
var sut = GetSut(CurrentHost, gestureTemplates, "auth/v2/");
var sut = GetSut(CurrentHost, settings => settings.GesturePathTemplateForDomain = gestureTemplates, "auth/v2/");

// Act
var result = sut.GetGesturePostbackRelativePath(123);
Expand All @@ -57,7 +57,7 @@ public void GetGesturePostbackRelativePath_UsesConfiguredDefault()
["Default"] = "/access/other",
[OtherHost] = "/access/specific-host"
};
var sut = GetSut(CurrentHost, gestureTemplates);
var sut = GetSut(CurrentHost, settings => settings.GesturePathTemplateForDomain = gestureTemplates);

// Act
var result = sut.GetGesturePostbackRelativePath(123);
Expand All @@ -76,7 +76,7 @@ public void GetGesturePostbackRelativePath_UsesSpecifiedHost_IfFound()
["Default"] = "/access/other",
[CurrentHost] = "/{customerId}/access/gesture"
};
var sut = GetSut(CurrentHost, gestureTemplates);
var sut = GetSut(CurrentHost, settings => settings.GesturePathTemplateForDomain = gestureTemplates);

// Act
var result = sut.GetGesturePostbackRelativePath(123);
Expand All @@ -87,21 +87,84 @@ public void GetGesturePostbackRelativePath_UsesSpecifiedHost_IfFound()
}

[Fact]
public void GetAccessServiceOAuthCallbackPath_Correct()
public void GetAccessServiceOAuthCallbackPath_HandlesNoConfiguredDefault()
{
// Arrange
var sut = GetSut(CurrentHost);
var templates = new Dictionary<string, string>
{
[OtherHost] = "/access/specific-host"
};
var accessService = new AccessService { Customer = 99, Name = "ghosts" };
var expected = new Uri("https://dlcs.test.example/access/99/ghosts/oauth2/callback");
var sut = GetSut(CurrentHost, settings => settings.OAuthCallbackPathTemplateForDomain = templates);

// Act
var result = sut.GetAccessServiceOAuthCallbackPath(accessService);

// Asset
result.Should().BeEquivalentTo(expected);
}

[Fact]
public void GetAccessServiceOAuthCallbackPath_HandlesNoConfiguredDefault_WithBaseBase()
{
// Arrange
var templates = new Dictionary<string, string>
{
[OtherHost] = "/access/specific-host"
};
var accessService = new AccessService { Customer = 99, Name = "ghosts" };
var expected = new Uri("https://dlcs.test.example/auth/v2/access/99/ghosts/oauth2/callback");
var sut = GetSut(CurrentHost, settings => settings.OAuthCallbackPathTemplateForDomain = templates, "auth/v2/");

// Act
var result = sut.GetAccessServiceOAuthCallbackPath(accessService);

// Asset
result.Should().BeEquivalentTo(expected);
}

[Fact]
public void GetAccessServiceOAuthCallbackPath_UsesConfiguredDefault()
{
// Arrange
var templates = new Dictionary<string, string>
{
["Default"] = "/access/other",
[OtherHost] = "/access/specific-host"
};
var accessService = new AccessService { Customer = 99, Name = "ghosts" };
var expected = new Uri("https://dlcs.test.example/access/other");
var sut = GetSut(CurrentHost, settings => settings.OAuthCallbackPathTemplateForDomain = templates, "auth/v2/");

// Act
var result = sut.GetAccessServiceOAuthCallbackPath(accessService);

// Assert
// Asset
result.Should().BeEquivalentTo(expected);
}

private UrlPathProvider GetSut(string host, Dictionary<string, string>? gestureTemplates = null, string? pathBase = null)
[Fact]
public void GetAccessServiceOAuthCallbackPath_UsesSpecifiedHost_IfFound()
{
// Arrange
var templates = new Dictionary<string, string>
{
["Default"] = "/access/other",
[CurrentHost] = "/{customerId}/callback/{accessService}"
};
var accessService = new AccessService { Customer = 99, Name = "ghosts" };
var expected = new Uri("https://dlcs.test.example/99/callback/ghosts");
var sut = GetSut(CurrentHost, settings => settings.OAuthCallbackPathTemplateForDomain = templates, "auth/v2/");

// Act
var result = sut.GetAccessServiceOAuthCallbackPath(accessService);

// Asset
result.Should().BeEquivalentTo(expected);
}

private UrlPathProvider GetSut(string host, Action<AuthSettings>? settingsConfig = null, string? pathBase = null)
{
var context = new DefaultHttpContext();
var request = context.Request;
Expand All @@ -110,7 +173,8 @@ private UrlPathProvider GetSut(string host, Dictionary<string, string>? gestureT
var contextAccessor = A.Fake<IHttpContextAccessor>();
A.CallTo(() => contextAccessor.HttpContext).Returns(context);

var authSettings = new AuthSettings { GesturePathTemplateForDomain = gestureTemplates ?? new() };
var authSettings = new AuthSettings();
settingsConfig?.Invoke(authSettings);
var apiSettings = Options.Create(new ApiSettings { Auth = authSettings, PathBase = pathBase });

return new UrlPathProvider(contextAccessor, apiSettings);
Expand Down
37 changes: 18 additions & 19 deletions src/IIIFAuth2/IIIFAuth2.API/Infrastructure/Web/UrlPathProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class UrlPathProvider : IUrlPathProvider
{
private readonly IHttpContextAccessor httpContextAccessor;
private readonly ApiSettings apiSettings;
private readonly Regex duplicateSlashRegex = new("(/)+", RegexOptions.Compiled);

public UrlPathProvider(IHttpContextAccessor httpContextAccessor, IOptions<ApiSettings> apiOptions)
{
Expand Down Expand Up @@ -95,13 +96,19 @@ public Uri GetAccessServiceLogoutPath(AccessService accessService)
/// <inheritdoc />
public Uri GetAccessServiceOAuthCallbackPath(AccessService accessService)
{
const string defaultPathTemplate = "/access/{customerId}/{accessService}/oauth2/callback";

var template = GetTemplate(apiSettings.Auth.OAuthCallbackPathTemplateForDomain, defaultPathTemplate);
var populatedTemplate = template
.Replace("{customerId}", accessService.Customer.ToString())
.Replace("{accessService}", accessService.Name);

var baseUrl = GetCurrentBaseUrl();
var path = $"/auth/v2/access/{accessService.Customer}/{accessService.Name}/oauth2/callback";
var builder = new UriBuilder(baseUrl)
{
Path = path
Path = populatedTemplate
};

return builder.Uri;
}

Expand All @@ -121,33 +128,25 @@ public Uri GetAccessTokenServicePath(int customerId)
/// <inheritdoc />
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)
{
var template = GetTemplate(host);
return template.Replace("{customerId}", customerId.ToString());
const string defaultPathTemplate = "/access/{customerId}/gesture";

var template = GetTemplate(apiSettings.Auth.GesturePathTemplateForDomain, defaultPathTemplate);
var populatedTemplate = template.Replace("{customerId}", customerId.ToString());
return new Uri(populatedTemplate, UriKind.Relative);
}

private string GetTemplate(string host)
private string GetTemplate(Dictionary<string, string> pathTemplates, string defaultPathTemplate)
{
const string defaultPathTemplate = "/access/{customerId}/gesture";
const string defaultKey = "Default";

var pathTemplates = apiSettings.Auth.GesturePathTemplateForDomain;
var request = httpContextAccessor.SafeHttpContext().Request;
var host = request.Host.Value;

if (pathTemplates.TryGetValue(host, out var hostTemplate)) return hostTemplate;
if (pathTemplates.TryGetValue(defaultKey, out var pathTemplate)) return pathTemplate;
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");
}

Expand Down
12 changes: 12 additions & 0 deletions src/IIIFAuth2/IIIFAuth2.API/Settings/ApiSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,17 @@ public class AuthSettings
/// </summary>
public int JwksTtl { get; set; } = 600;

/// <summary>
/// Dictionary that allows control of domain-specific significant gesture paths. Default value is
/// /access/{customerId}/gesture.
/// Replacement values: {customerId}
/// </summary>
public Dictionary<string, string> GesturePathTemplateForDomain { get; set; } = new();

/// <summary>
/// Dictionary that allows control of domain-specific oauth callback paths. Default value is
/// /access/{customerId}/{accessService}/oauth2/callback.
/// Replacement values: {customerId} and {accessService}
/// </summary>
public Dictionary<string, string> OAuthCallbackPathTemplateForDomain { get; set; } = new();
}

0 comments on commit 7f3d117

Please sign in to comment.