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