From d5e7dc8a7e8d49e20a216559beddef96a57719bb Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Mon, 2 Sep 2024 13:27:01 +0100
Subject: [PATCH 1/8] Add JWT auth parallel to existing basic auth
---
src/protagonist/API/API.csproj | 1 +
.../Auth/DlcsBasicAuthenticationHandler.cs | 176 +++++++++++++-----
src/protagonist/API/Auth/JwtAuthHelper.cs | 16 ++
src/protagonist/API/Startup.cs | 1 +
.../DLCS.Core/Settings/DlcsSettings.cs | 10 +
5 files changed, 156 insertions(+), 48 deletions(-)
create mode 100644 src/protagonist/API/Auth/JwtAuthHelper.cs
diff --git a/src/protagonist/API/API.csproj b/src/protagonist/API/API.csproj
index 9fb96f19f..603f58207 100644
--- a/src/protagonist/API/API.csproj
+++ b/src/protagonist/API/API.csproj
@@ -23,6 +23,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
index e46fe1837..c50ea822e 100644
--- a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
+++ b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
@@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.JsonWebTokens;
namespace API.Auth;
@@ -22,7 +23,7 @@ namespace API.Auth;
/// to an anonymous user. Another example: https://api.dlcs.io/originStrategies
/// - An admin API key is allowed to make any valid API call.
/// - A customer path is one that starts /customers/n/, where n is the customer id.
-/// Customer-specific resources such as images, auth services, etc live under these paths.
+/// Customer-specific resources such as images, auth services, etc. live under these paths.
/// - Only an admin key or a key belonging to that customer can call these paths.
/// - A customer path is never anonymous.
///
@@ -35,6 +36,9 @@ public class DlcsBasicAuthenticationHandler : AuthenticationHandler
/// Deduces the caller's claims
@@ -50,11 +54,13 @@ public DlcsBasicAuthenticationHandler(
UrlEncoder encoder,
ISystemClock clock,
ICustomerRepository customerRepository,
- DlcsApiAuth dlcsApiAuth)
+ DlcsApiAuth dlcsApiAuth,
+ JwtAuthHelper authHelper)
: base(options, logger, encoder, clock)
{
this.customerRepository = customerRepository;
this.dlcsApiAuth = dlcsApiAuth;
+ this.authHelper = authHelper;
}
///
@@ -67,12 +73,13 @@ protected override async Task HandleAuthenticateAsync()
var endpoint = Context.GetEndpoint();
if (endpoint?.Metadata.GetMetadata() != null)
return AuthenticateResult.NoResult();
-
+
// ...but any not marked must have the auth header
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header in request");
- var headerValue = Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BasicScheme);
+ var headerValue = Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BasicScheme)
+ ?? Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BearerTokenScheme);
if (headerValue == null)
{
return AuthenticateResult.Fail("Missing Authorization Header in request");
@@ -88,21 +95,29 @@ protected override async Task HandleAuthenticateAsync()
resourceCustomerId = result;
}
}
- var apiCaller = GetApiCaller(headerValue);
- if (apiCaller == null)
- {
- return AuthenticateResult.Fail("Invalid credentials");
- }
-
- var customerForKey = await customerRepository.GetCustomerForKey(apiCaller.Key, resourceCustomerId);
- if (customerForKey == null)
+
+ switch (await GetApiCaller(headerValue, resourceCustomerId))
{
- return AuthenticateResult.Fail("No customer found for this key that is permitted to access this resource");
+ case null:
+ return AuthenticateResult.Fail("Invalid credentials");
+ case FailedCaller fail:
+ return AuthenticateResult.Fail(fail.Message);
+
+ // Success:
+ case ApiCaller apiCaller:
+ return AuthenticateApiCaller(apiCaller, resourceCustomerId);
+
+ default:
+ throw new InvalidOperationException();
}
+ }
- if (apiCaller.Secret != dlcsApiAuth.GetApiSecret(customerForKey, Options.Salt, apiCaller.Key))
+ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resourceCustomerId)
+ {
+ if (apiCaller.Customer is not { } customer)
{
- return AuthenticateResult.Fail("Invalid credentials");
+ Logger.LogError("A non-fail ApiCaller returned with null Customer field");
+ return AuthenticateResult.Fail("Unexpected error");
}
// We still have some checks to do before authenticating this user.
@@ -110,13 +125,14 @@ protected override async Task HandleAuthenticateAsync()
if (resourceCustomerId.HasValue)
{
// the request is for a particular customer's resource (e.g., an asset)
- if (customerForKey.Id != resourceCustomerId.Value)
+ if (customer.Id != resourceCustomerId.Value)
{
// ... but the requester is not this customer. Only proceed if they are admin.
- if (!customerForKey.Administrator)
+ if (!customer.Administrator)
{
return AuthenticateResult.Fail("Only admin user may access this customer's resource.");
}
+
Logger.LogDebug("Admin key accessing a customer's resource");
}
}
@@ -124,13 +140,14 @@ protected override async Task HandleAuthenticateAsync()
{
// The request isn't for a customer resource (i.e., under the path /customers/n/).
// Only proceed if they are admin.
- if (!customerForKey.Administrator)
+ if (!customer.Administrator)
{
return AuthenticateResult.Fail("Only admin user may access this shared resource");
}
+
Logger.LogDebug("Admin key accessing a shared resource");
}
-
+
// At this point our *authentication* has passed.
// Downstream handlers may still refuse to *authorise* the request for other reasons
// (it can still end up a 401)
@@ -138,42 +155,100 @@ protected override async Task HandleAuthenticateAsync()
var claims = new List
{
- new (ClaimTypes.Name, customerForKey.Name), // TODO - should these be the other way round?
- new (ClaimTypes.NameIdentifier, apiCaller.Key), // ^^^
- new (ClaimsPrincipalUtils.Claims.Customer, customerForKey.Id.ToString()),
- new (ClaimTypes.Role, ClaimsPrincipalUtils.Roles.Customer),
+ new(ClaimTypes.Name, customer.Name), // TODO - should these be the other way round?
+ new(ClaimTypes.NameIdentifier, apiCaller.Key), // ^^^
+ new(ClaimsPrincipalUtils.Claims.Customer, customer.Id.ToString()),
+ new(ClaimTypes.Role, ClaimsPrincipalUtils.Roles.Customer),
};
- if (customerForKey.Administrator)
+ if (customer.Administrator)
{
- claims.Add(new Claim(ClaimTypes.Role, ClaimsPrincipalUtils.Roles.Admin));
+ claims.Add(new(ClaimTypes.Role, ClaimsPrincipalUtils.Roles.Admin));
}
+
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
- private ApiCaller? GetApiCaller(AuthenticationHeaderValue headerValue)
+ private async Task GetApiCaller(AuthenticationHeaderValue headerValue, int? customerIdHint)
{
- try
+ switch (headerValue.Scheme)
{
- if (headerValue.Parameter == null) return null;
-
- var authHeader = headerValue.Parameter.DecodeBase64();
- string[] keyAndSecret = authHeader.Split(':');
- var apiCaller = new ApiCaller(keyAndSecret[0], keyAndSecret[1]);
- if (apiCaller.Key.HasText() && apiCaller.Secret.HasText())
- {
- return apiCaller;
- }
+ case AuthenticationHeaderUtils.BasicScheme:
+ return await GetApiCallerFromBasic(headerValue.Parameter, customerIdHint);
+ case AuthenticationHeaderUtils.BearerTokenScheme:
+ return await GetApiCallerFromJwt(headerValue.Parameter);
+ }
- return null;
+ Logger.LogError("Could not parse auth header");
+ return null;
+ }
+
+ private async Task GetApiCallerFromBasic(string? headerValueParameter, int? customerIdHint)
+ {
+ if (headerValueParameter?.DecodeBase64().Split(':') is not
+ [{Length: > 0} key, {Length: > 0} secret]) return null;
+
+ var customerForKey = await customerRepository.GetCustomerForKey(key, customerIdHint);
+ if (customerForKey == null)
+ {
+ return new FailedCaller("No customer found for this key that is permitted to access this resource");
}
- catch
+
+ if (secret != dlcsApiAuth.GetApiSecret(customerForKey, Options.Salt, key))
{
- Logger.LogError("Could not parse auth header");
+ return new FailedCaller("Invalid credentials");
}
- return null;
+
+ return new ApiCaller(key, customerForKey);
+ }
+
+ private async Task GetApiCallerFromJwt(string? token)
+ {
+ if (authHelper.SigningCredentials?.Key is not { } signingKey)
+ return new FailedCaller("JWT not enabled");
+
+ if (token is null)
+ return new FailedCaller("JWT missing");
+
+ var result = await jwtHandler.ValidateTokenAsync(token, new()
+ {
+ IssuerSigningKey = signingKey,
+ ValidateAudience = false,
+ ValidateIssuer = true,
+ ValidIssuers = authHelper.ValidIssuers
+ });
+
+ if (!result.IsValid)
+ {
+ return new FailedCaller("Invalid JWT used");
+ }
+
+ if (!result.Claims.TryGetValue(JwtRegisteredClaimNames.Sub, out var subUrnObj) ||
+ subUrnObj is not string subUrn)
+ {
+ return new FailedCaller("JWT missing sub field");
+ }
+
+ if (subUrn.Split(':') is not [_, var nid, var nss, var id, ..])
+ {
+ return new FailedCaller("JWT sub in incorrect urn format");
+ }
+
+ // Future integration: nid/nss/id can be used for granular permissions
+ // For now, we have 1 customer, the Customer Portal, so "hardcode" the
+ // verification.
+
+ // We only support the following subjects: urn:dlcs:user:
+ if (!"dlcs".Equals(nid) || !"user".Equals(nss))
+ return new FailedCaller("Unsupported/unauthorised token data");
+
+ var customer = await customerRepository.GetCustomer(id);
+ if (customer is null)
+ return new FailedCaller("Customer not found");
+
+ return new ApiCaller(customer.Keys.First(), customer);
}
///
@@ -186,17 +261,22 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
}
}
+///
+/// Can be or
+///
+public interface IApiCaller;
+
///
/// Represents the credentials in the API request, the basic auth key:secret pair.
///
-public class ApiCaller
+public class ApiCaller(string key, Customer? customer) : IApiCaller
{
- public ApiCaller(string key, string secret)
- {
- Key = key;
- Secret = secret;
- }
+ public string Key { get; set; } = key;
- public string Key { get; set; }
- public string Secret { get; set; }
+ public Customer? Customer { get; } = customer;
+}
+
+public class FailedCaller(string message) : IApiCaller
+{
+ public string Message { get; } = message;
}
\ No newline at end of file
diff --git a/src/protagonist/API/Auth/JwtAuthHelper.cs b/src/protagonist/API/Auth/JwtAuthHelper.cs
new file mode 100644
index 000000000..d0a2f0cb6
--- /dev/null
+++ b/src/protagonist/API/Auth/JwtAuthHelper.cs
@@ -0,0 +1,16 @@
+using DLCS.Core.Settings;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+
+namespace API.Auth;
+
+public class JwtAuthHelper(IOptions dlcsSettings)
+{
+ public SigningCredentials? SigningCredentials { get; } = dlcsSettings.Value.JwtKey is {Length: > 0} key
+ ? new(
+ new SymmetricSecurityKey(Convert.FromBase64String(key)),
+ SecurityAlgorithms.HmacSha256Signature)
+ : null;
+
+ public string[] ValidIssuers { get; } = dlcsSettings.Value.JwtValidIssuers.ToArray();
+}
\ No newline at end of file
diff --git a/src/protagonist/API/Startup.cs b/src/protagonist/API/Startup.cs
index f0f17c2ba..1d424e531 100644
--- a/src/protagonist/API/Startup.cs
+++ b/src/protagonist/API/Startup.cs
@@ -68,6 +68,7 @@ public void ConfigureServices(IServiceCollection services)
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddTransient(s => s.GetRequiredService().HttpContext.User)
.AddCaching(cacheSettings)
diff --git a/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs b/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs
index a79837154..7fc721735 100644
--- a/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs
+++ b/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs
@@ -37,4 +37,14 @@ public class DlcsSettings
/// URL format for generating manifests for single assets
///
public string SingleAssetManifestTemplate { get; set; }
+
+ ///
+ /// 256bit or longer, Base64 encoded, JWT secret
+ ///
+ public string? JwtKey { get; set; }
+
+ ///
+ /// List of valid issuers of JWT for authentication
+ ///
+ public ICollection JwtValidIssuers { get; set; } = [];
}
\ No newline at end of file
From 1cf7be8e1b45753fab7a99c3023dec1e8745445c Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Mon, 2 Sep 2024 14:33:21 +0100
Subject: [PATCH 2/8] Update DlcsBasicAuthenticationHandler.cs
---
.../Auth/DlcsBasicAuthenticationHandler.cs | 29 ++++++++-----------
1 file changed, 12 insertions(+), 17 deletions(-)
diff --git a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
index c50ea822e..c178eee09 100644
--- a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
+++ b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
@@ -80,6 +80,7 @@ protected override async Task HandleAuthenticateAsync()
var headerValue = Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BasicScheme)
?? Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BearerTokenScheme);
+
if (headerValue == null)
{
return AuthenticateResult.Fail("Missing Authorization Header in request");
@@ -88,28 +89,22 @@ protected override async Task HandleAuthenticateAsync()
// for a path like /customers/23/queue, the resourceCustomerId is 23.
// This isn't necessarily the customer that owns the api key being used on the call!
int? resourceCustomerId = null;
- if (Request.RouteValues.TryGetValue("customerId", out var customerIdRouteVal))
+ if (Request.RouteValues.TryGetValue("customerId", out var customerIdRouteVal)
+ && customerIdRouteVal != null
+ && int.TryParse(customerIdRouteVal.ToString(), out var result))
{
- if (customerIdRouteVal != null && int.TryParse(customerIdRouteVal.ToString(), out int result))
- {
- resourceCustomerId = result;
- }
+ resourceCustomerId = result;
}
- switch (await GetApiCaller(headerValue, resourceCustomerId))
+ return await GetApiCaller(headerValue, resourceCustomerId) switch
{
- case null:
- return AuthenticateResult.Fail("Invalid credentials");
- case FailedCaller fail:
- return AuthenticateResult.Fail(fail.Message);
-
+ null => AuthenticateResult.Fail("Invalid credentials"),
+ FailedCaller fail => AuthenticateResult.Fail(fail.Message),
// Success:
- case ApiCaller apiCaller:
- return AuthenticateApiCaller(apiCaller, resourceCustomerId);
-
- default:
- throw new InvalidOperationException();
- }
+ ApiCaller apiCaller => AuthenticateApiCaller(apiCaller, resourceCustomerId),
+ // Unlikely:
+ _ => throw new InvalidOperationException()
+ };
}
private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resourceCustomerId)
From c091c354f375035287adfe68847880a786e12e5d Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Mon, 2 Sep 2024 17:05:10 +0100
Subject: [PATCH 3/8] Update appsettings-Development-Example.json
---
src/protagonist/API/appsettings-Development-Example.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/protagonist/API/appsettings-Development-Example.json b/src/protagonist/API/appsettings-Development-Example.json
index c44bd2e3a..dc895d6e2 100644
--- a/src/protagonist/API/appsettings-Development-Example.json
+++ b/src/protagonist/API/appsettings-Development-Example.json
@@ -32,6 +32,7 @@
},
"PathBase": "",
"Salt": "********",
+ "ApiSalt": "********",
"PageSize": 100,
"DefaultLegacySupport": true,
"CustomerOverrides": {
From 0c27e1552e428c60692c4b6db16260ba2fedc986 Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Mon, 2 Sep 2024 17:05:15 +0100
Subject: [PATCH 4/8] Update DlcsBasicAuthenticationHandler.cs
---
src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
index c178eee09..4cacc37c1 100644
--- a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
+++ b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
@@ -239,7 +239,10 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
if (!"dlcs".Equals(nid) || !"user".Equals(nss))
return new FailedCaller("Unsupported/unauthorised token data");
- var customer = await customerRepository.GetCustomer(id);
+ if (!int.TryParse(id, out var customerId))
+ return new FailedCaller("Invalid customer id format");
+
+ var customer = await customerRepository.GetCustomer(customerId);
if (customer is null)
return new FailedCaller("Customer not found");
From 5888e8e0381ebb049c7834e2a35db2123b2abb3c Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Mon, 2 Sep 2024 17:58:02 +0100
Subject: [PATCH 5/8] Make jwthandler static, so it doesn't get recreated
needlessly every time
---
src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
index 4cacc37c1..b539f473d 100644
--- a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
+++ b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
@@ -38,7 +38,7 @@ public class DlcsBasicAuthenticationHandler : AuthenticationHandler
/// Deduces the caller's claims
@@ -207,7 +207,7 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
if (token is null)
return new FailedCaller("JWT missing");
- var result = await jwtHandler.ValidateTokenAsync(token, new()
+ var result = await JwtHandler.ValidateTokenAsync(token, new()
{
IssuerSigningKey = signingKey,
ValidateAudience = false,
From 23cf66f22e4234f6c92b083af5e401495d808a0f Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Wed, 4 Sep 2024 16:51:54 +0100
Subject: [PATCH 6/8] Applied PR Feedback
---
src/protagonist/.editorconfig | 3 +-
.../Auth/DlcsBasicAuthenticationHandler.cs | 34 ++++++++++++++++---
src/protagonist/API/Auth/JwtAuthHelper.cs | 20 +++++++----
.../DLCS.Core/Settings/DlcsSettings.cs | 2 +-
4 files changed, 45 insertions(+), 14 deletions(-)
diff --git a/src/protagonist/.editorconfig b/src/protagonist/.editorconfig
index 041a5f244..edfb98422 100644
--- a/src/protagonist/.editorconfig
+++ b/src/protagonist/.editorconfig
@@ -1,2 +1,3 @@
[*.cs]
-csharp_style_namespace_declarations=file_scoped:warning
\ No newline at end of file
+csharp_style_namespace_declarations=file_scoped:warning
+csharp_prefer_braces = true:suggestion
\ No newline at end of file
diff --git a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
index b539f473d..1347d6947 100644
--- a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
+++ b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
@@ -72,11 +72,15 @@ protected override async Task HandleAuthenticateAsync()
// Not all API paths require auth...
var endpoint = Context.GetEndpoint();
if (endpoint?.Metadata.GetMetadata() != null)
+ {
return AuthenticateResult.NoResult();
+ }
// ...but any not marked must have the auth header
if (!Request.Headers.ContainsKey("Authorization"))
+ {
return AuthenticateResult.Fail("Missing Authorization Header in request");
+ }
var headerValue = Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BasicScheme)
?? Request.GetAuthHeaderValue(AuthenticationHeaderUtils.BearerTokenScheme);
@@ -89,7 +93,7 @@ protected override async Task HandleAuthenticateAsync()
// for a path like /customers/23/queue, the resourceCustomerId is 23.
// This isn't necessarily the customer that owns the api key being used on the call!
int? resourceCustomerId = null;
- if (Request.RouteValues.TryGetValue("customerId", out var customerIdRouteVal)
+ if (Request.RouteValues.TryGetValue("customerId", out var customerIdRouteVal)
&& customerIdRouteVal != null
&& int.TryParse(customerIdRouteVal.ToString(), out var result))
{
@@ -183,7 +187,10 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
private async Task GetApiCallerFromBasic(string? headerValueParameter, int? customerIdHint)
{
if (headerValueParameter?.DecodeBase64().Split(':') is not
- [{Length: > 0} key, {Length: > 0} secret]) return null;
+ [{Length: > 0} key, {Length: > 0} secret])
+ {
+ return null;
+ }
var customerForKey = await customerRepository.GetCustomerForKey(key, customerIdHint);
if (customerForKey == null)
@@ -197,15 +204,20 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
}
return new ApiCaller(key, customerForKey);
+
}
private async Task GetApiCallerFromJwt(string? token)
{
if (authHelper.SigningCredentials?.Key is not { } signingKey)
+ {
return new FailedCaller("JWT not enabled");
+ }
if (token is null)
+ {
return new FailedCaller("JWT missing");
+ }
var result = await JwtHandler.ValidateTokenAsync(token, new()
{
@@ -226,6 +238,12 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
return new FailedCaller("JWT missing sub field");
}
+ // RFC 8141:
+ // NID - Namespace Identifier - here "dlcs" to have own namespace
+ // NSS - Namespace Specific String - currently we use "user" to inform that the next part is user (customer) id
+ const string dlcsUrnNamespace = "dlcs";
+ const string dlcsUrnUser = "user";
+
if (subUrn.Split(':') is not [_, var nid, var nss, var id, ..])
{
return new FailedCaller("JWT sub in incorrect urn format");
@@ -236,15 +254,21 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
// verification.
// We only support the following subjects: urn:dlcs:user:
- if (!"dlcs".Equals(nid) || !"user".Equals(nss))
+ if (!dlcsUrnNamespace.Equals(nid) || !dlcsUrnUser.Equals(nss))
+ {
return new FailedCaller("Unsupported/unauthorised token data");
+ }
if (!int.TryParse(id, out var customerId))
+ {
return new FailedCaller("Invalid customer id format");
-
+ }
+
var customer = await customerRepository.GetCustomer(customerId);
if (customer is null)
+ {
return new FailedCaller("Customer not found");
+ }
return new ApiCaller(customer.Keys.First(), customer);
}
@@ -269,7 +293,7 @@ public interface IApiCaller;
///
public class ApiCaller(string key, Customer? customer) : IApiCaller
{
- public string Key { get; set; } = key;
+ public string Key { get; } = key;
public Customer? Customer { get; } = customer;
}
diff --git a/src/protagonist/API/Auth/JwtAuthHelper.cs b/src/protagonist/API/Auth/JwtAuthHelper.cs
index d0a2f0cb6..caf8947a7 100644
--- a/src/protagonist/API/Auth/JwtAuthHelper.cs
+++ b/src/protagonist/API/Auth/JwtAuthHelper.cs
@@ -4,13 +4,19 @@
namespace API.Auth;
-public class JwtAuthHelper(IOptions dlcsSettings)
+public class JwtAuthHelper
{
- public SigningCredentials? SigningCredentials { get; } = dlcsSettings.Value.JwtKey is {Length: > 0} key
- ? new(
- new SymmetricSecurityKey(Convert.FromBase64String(key)),
- SecurityAlgorithms.HmacSha256Signature)
- : null;
+ public JwtAuthHelper(IOptions dlcsSettings)
+ {
+ SigningCredentials = dlcsSettings.Value.JwtKey is {Length: > 0} key
+ ? new(
+ new SymmetricSecurityKey(Convert.FromBase64String(key)),
+ SecurityAlgorithms.HmacSha256Signature)
+ : null;
+ ValidIssuers = dlcsSettings.Value.JwtValidIssuers.ToArray();
+ }
- public string[] ValidIssuers { get; } = dlcsSettings.Value.JwtValidIssuers.ToArray();
+ public SigningCredentials? SigningCredentials { get; }
+
+ public string[] ValidIssuers { get; }
}
\ No newline at end of file
diff --git a/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs b/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs
index 7fc721735..32916ca72 100644
--- a/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs
+++ b/src/protagonist/DLCS.Core/Settings/DlcsSettings.cs
@@ -46,5 +46,5 @@ public class DlcsSettings
///
/// List of valid issuers of JWT for authentication
///
- public ICollection JwtValidIssuers { get; set; } = [];
+ public ICollection JwtValidIssuers { get; set; } = Array.Empty();
}
\ No newline at end of file
From 15e928cb827dfa2c7febc8508c3abc14ca5c8151 Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Thu, 5 Sep 2024 11:12:13 +0100
Subject: [PATCH 7/8] Rewrite bits into C#10
---
.../Auth/DlcsBasicAuthenticationHandler.cs | 46 +++++++++++++++----
1 file changed, 36 insertions(+), 10 deletions(-)
diff --git a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
index 1347d6947..179be75e0 100644
--- a/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
+++ b/src/protagonist/API/Auth/DlcsBasicAuthenticationHandler.cs
@@ -186,12 +186,14 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
private async Task GetApiCallerFromBasic(string? headerValueParameter, int? customerIdHint)
{
- if (headerValueParameter?.DecodeBase64().Split(':') is not
- [{Length: > 0} key, {Length: > 0} secret])
+ var parts = headerValueParameter?.DecodeBase64().Split(':');
+ if (parts?.Length != 2 || string.IsNullOrEmpty(parts[0]) || string.IsNullOrEmpty(parts[1]))
{
return null;
}
-
+ var key = parts[0];
+ var secret = parts[1];
+
var customerForKey = await customerRepository.GetCustomerForKey(key, customerIdHint);
if (customerForKey == null)
{
@@ -244,11 +246,22 @@ private AuthenticateResult AuthenticateApiCaller(ApiCaller apiCaller, int? resou
const string dlcsUrnNamespace = "dlcs";
const string dlcsUrnUser = "user";
- if (subUrn.Split(':') is not [_, var nid, var nss, var id, ..])
+ // In modern C#:
+ // if (subUrn.Split(':') is not [_, var nid, var nss, var id, ..])
+ // {
+ // return new FailedCaller("JWT sub in incorrect urn format");
+ // }
+
+ var parts = subUrn.Split(':');
+ if (parts.Length < 4)
{
return new FailedCaller("JWT sub in incorrect urn format");
}
+ var nid = parts[1];
+ var nss = parts[2];
+ var id = parts[3];
+
// Future integration: nid/nss/id can be used for granular permissions
// For now, we have 1 customer, the Customer Portal, so "hardcode" the
// verification.
@@ -286,19 +299,32 @@ protected override Task HandleChallengeAsync(AuthenticationProperties properties
///
/// Can be or
///
-public interface IApiCaller;
+public interface IApiCaller
+{
+}
///
/// Represents the credentials in the API request, the basic auth key:secret pair.
///
-public class ApiCaller(string key, Customer? customer) : IApiCaller
+public class ApiCaller : IApiCaller
{
- public string Key { get; } = key;
+ public ApiCaller(string key, Customer? customer)
+ {
+ Key = key;
+ Customer = customer;
+ }
+
+ public string Key { get; }
- public Customer? Customer { get; } = customer;
+ public Customer? Customer { get; }
}
-public class FailedCaller(string message) : IApiCaller
+public class FailedCaller : IApiCaller
{
- public string Message { get; } = message;
+ public FailedCaller(string message)
+ {
+ Message = message;
+ }
+
+ public string Message { get; }
}
\ No newline at end of file
From 080bf9cdd58aeaaaac4912c840594d8bf4163454 Mon Sep 17 00:00:00 2001
From: p-kaczynski
Date: Thu, 5 Sep 2024 11:12:43 +0100
Subject: [PATCH 8/8] Set lang version to 10 (.net6) instead of default
Default defaults to latest major, which is like 13 or 14rn xD
---
src/protagonist/API/API.csproj | 2 +-
src/protagonist/DLCS.Core.Tests/DLCS.Core.Tests.csproj | 2 +-
src/protagonist/DLCS.Core/DLCS.Core.csproj | 2 +-
src/protagonist/DLCS.Model.Tests/DLCS.Model.Tests.csproj | 2 +-
src/protagonist/DLCS.Model/DLCS.Model.csproj | 2 +-
.../DLCS.Repository.Tests/DLCS.Repository.Tests.csproj | 2 +-
src/protagonist/DLCS.Repository/DLCS.Repository.csproj | 2 +-
src/protagonist/DLCS.Web.Tests/DLCS.Web.Tests.csproj | 2 +-
src/protagonist/DLCS.Web/DLCS.Web.csproj | 2 +-
src/protagonist/Portal.Tests/Portal.Tests.csproj | 2 +-
src/protagonist/Thumbs/Thumbs.csproj | 2 +-
11 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/protagonist/API/API.csproj b/src/protagonist/API/API.csproj
index 603f58207..ab1afaa37 100644
--- a/src/protagonist/API/API.csproj
+++ b/src/protagonist/API/API.csproj
@@ -4,7 +4,7 @@
net6.0
enable
true
- default
+ 10.0
$(NoWarn);1591
diff --git a/src/protagonist/DLCS.Core.Tests/DLCS.Core.Tests.csproj b/src/protagonist/DLCS.Core.Tests/DLCS.Core.Tests.csproj
index 08b8d6654..2a364d92a 100644
--- a/src/protagonist/DLCS.Core.Tests/DLCS.Core.Tests.csproj
+++ b/src/protagonist/DLCS.Core.Tests/DLCS.Core.Tests.csproj
@@ -5,7 +5,7 @@
false
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Core/DLCS.Core.csproj b/src/protagonist/DLCS.Core/DLCS.Core.csproj
index 580346365..92dae7c21 100644
--- a/src/protagonist/DLCS.Core/DLCS.Core.csproj
+++ b/src/protagonist/DLCS.Core/DLCS.Core.csproj
@@ -3,7 +3,7 @@
net6.0
enable
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Model.Tests/DLCS.Model.Tests.csproj b/src/protagonist/DLCS.Model.Tests/DLCS.Model.Tests.csproj
index 1e724c294..5bf9b6bdf 100644
--- a/src/protagonist/DLCS.Model.Tests/DLCS.Model.Tests.csproj
+++ b/src/protagonist/DLCS.Model.Tests/DLCS.Model.Tests.csproj
@@ -5,7 +5,7 @@
false
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Model/DLCS.Model.csproj b/src/protagonist/DLCS.Model/DLCS.Model.csproj
index f5ef8b4e2..0d5d4037e 100644
--- a/src/protagonist/DLCS.Model/DLCS.Model.csproj
+++ b/src/protagonist/DLCS.Model/DLCS.Model.csproj
@@ -3,7 +3,7 @@
net6.0
enable
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Repository.Tests/DLCS.Repository.Tests.csproj b/src/protagonist/DLCS.Repository.Tests/DLCS.Repository.Tests.csproj
index e73ae8859..685068aa7 100644
--- a/src/protagonist/DLCS.Repository.Tests/DLCS.Repository.Tests.csproj
+++ b/src/protagonist/DLCS.Repository.Tests/DLCS.Repository.Tests.csproj
@@ -5,7 +5,7 @@
false
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Repository/DLCS.Repository.csproj b/src/protagonist/DLCS.Repository/DLCS.Repository.csproj
index 659342888..110588fa1 100644
--- a/src/protagonist/DLCS.Repository/DLCS.Repository.csproj
+++ b/src/protagonist/DLCS.Repository/DLCS.Repository.csproj
@@ -3,7 +3,7 @@
net6.0
enable
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Web.Tests/DLCS.Web.Tests.csproj b/src/protagonist/DLCS.Web.Tests/DLCS.Web.Tests.csproj
index 98faf58f1..3f33ff257 100644
--- a/src/protagonist/DLCS.Web.Tests/DLCS.Web.Tests.csproj
+++ b/src/protagonist/DLCS.Web.Tests/DLCS.Web.Tests.csproj
@@ -5,7 +5,7 @@
false
- default
+ 10.0
diff --git a/src/protagonist/DLCS.Web/DLCS.Web.csproj b/src/protagonist/DLCS.Web/DLCS.Web.csproj
index 6fbdcd4d0..28ce81933 100644
--- a/src/protagonist/DLCS.Web/DLCS.Web.csproj
+++ b/src/protagonist/DLCS.Web/DLCS.Web.csproj
@@ -3,7 +3,7 @@
net6.0
enable
- default
+ 10.0
diff --git a/src/protagonist/Portal.Tests/Portal.Tests.csproj b/src/protagonist/Portal.Tests/Portal.Tests.csproj
index 5e3974e3b..cd4c81d31 100644
--- a/src/protagonist/Portal.Tests/Portal.Tests.csproj
+++ b/src/protagonist/Portal.Tests/Portal.Tests.csproj
@@ -4,7 +4,7 @@
net6.0
false
- default
+ 10.0
diff --git a/src/protagonist/Thumbs/Thumbs.csproj b/src/protagonist/Thumbs/Thumbs.csproj
index c6696d973..613a11d50 100644
--- a/src/protagonist/Thumbs/Thumbs.csproj
+++ b/src/protagonist/Thumbs/Thumbs.csproj
@@ -4,7 +4,7 @@
net6.0
Linux
enable
- default
+ 10.0