Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow oauth2 redirect_url to differ per domain #40

Merged
merged 2 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
Loading