From 7daa7c23ad3add46f1c767d1bca432c0c3e092aa Mon Sep 17 00:00:00 2001 From: Oleksii Nikiforov Date: Tue, 23 Apr 2024 18:11:37 +0300 Subject: [PATCH] patch: Update dependencies (#61) * Update dependencies * Add KeycloakFormatBinder --------- Co-authored-by: Oleksii Nikiforov --- .editorconfig | 4 +- KeycloakAuthorizationServicesDotNet.sln | 7 ++ global.json | 4 +- samples/AuthGettingStarted/Program.cs | 50 ++++++++------ .../ServiceCollectionExtensions.OpenApi.cs | 34 +++++---- .../AuthorizationAndCleanArchitecture.csproj | 1 - samples/Blazor/Server/Program.cs | 4 +- src/Directory.Build.props | 8 ++- src/Directory.Packages.props | 22 +++--- .../Claims/.editorconfig | 2 + .../KeycloakConfigurationProvider.cs | 16 +++-- ...eycloak.AuthServices.Authentication.csproj | 3 - .../ServiceCollectionExtensions.cs | 69 ++++++++++--------- ...Keycloak.AuthServices.Authorization.csproj | 3 - .../ServiceCollectionExtensions.cs | 13 ++-- .../Keycloak.AuthServices.Common.csproj | 4 +- .../KeycloakConstants.cs | 4 +- .../KeycloakInstallationOptions.cs | 41 ++++++++--- .../KeycloakResourceClaimsExtensions.cs | 36 ++++++---- .../ResourceAccessCollection.cs | 7 +- .../Constants/KeycloakClientApiConstants.cs | 6 +- .../Admin/IKeycloakClient.cs | 7 +- .../Admin/IKeycloakGroupClient.cs | 13 ++-- .../Admin/IKeycloakUserClient.cs | 31 ++++++--- .../Admin/Models/Resources/Resource.cs | 3 +- .../Models/Resources/ResourceResponse.cs | 1 + .../Groups/GetGroupRequestParameters.cs | 23 +++---- .../AuthZ/IKeycloakProtectionClient.cs | 6 +- .../AuthZ/KeycloakProtectionClient.cs | 24 ++++--- .../AuthZ/ServiceCollectionExtensions.cs | 9 ++- .../AccessTokenPropagationExtensions.cs | 12 ++-- .../AccessTokenPropagationHandler.cs | 17 ++--- .../Keycloak.AuthServices.Sdk.csproj | 3 - tests/Directory.Build.props | 8 +++ .../KeycloakRolesClaimsTransformationTests.cs | 23 ++++--- ...k.AuthServices.Authentication.Tests.csproj | 8 --- .../Keycloak.AuthServices.Common.Tests.csproj | 12 ++++ .../KeycloakInstallationOptionsTests.cs | 42 +++++++++++ .../appsettings.json | 22 ++++++ .../Keycloak.AuthServices.Sdk.Tests.csproj | 6 -- 40 files changed, 389 insertions(+), 219 deletions(-) create mode 100644 src/Keycloak.AuthServices.Authentication/Claims/.editorconfig create mode 100644 tests/Keycloak.AuthServices.Common.Tests/Keycloak.AuthServices.Common.Tests.csproj create mode 100644 tests/Keycloak.AuthServices.Common.Tests/KeycloakInstallationOptionsTests.cs create mode 100644 tests/Keycloak.AuthServices.Common.Tests/appsettings.json diff --git a/.editorconfig b/.editorconfig index 6ea648e5..b7d458e4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,4 @@ + ########################################## # Common Settings ########################################## @@ -75,7 +76,7 @@ dotnet_analyzer_diagnostic.severity = warning # Motivation https://github.com/dotnet/roslyn/blob/9f87b444da9c48a4d492b19f8337339056bf2b95/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs ########################################## # IDE0055 Fix formatting -dotnet_diagnostic.IDE0055.severity = error +dotnet_diagnostic.IDE0055.severity = suggestion # IDE005_gen: Remove unnecessary usings in generated code dotnet_diagnostic.IDE0005.severity = error # IDE0065: Using directives must be placed outside of a namespace declaration @@ -226,7 +227,6 @@ csharp_using_directive_placement = inside_namespace:warning csharp_prefer_static_local_function = true:warning csharp_prefer_simple_using_statement = true:suggestion dotnet_diagnostic.IDE0063.severity = suggestion - ########################################## # .NET Formatting Conventions # https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions diff --git a/KeycloakAuthorizationServicesDotNet.sln b/KeycloakAuthorizationServicesDotNet.sln index f605a502..172e41a4 100644 --- a/KeycloakAuthorizationServicesDotNet.sln +++ b/KeycloakAuthorizationServicesDotNet.sln @@ -60,6 +60,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keycloak.AuthServices.Sdk.T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Authentication.Tests", "tests\Keycloak.AuthServices.Authentication.Tests\Keycloak.AuthServices.Authentication.Tests.csproj", "{FE34728A-25AA-44E1-A3A6-AB500307C406}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Common.Tests", "tests\Keycloak.AuthServices.Common.Tests\Keycloak.AuthServices.Common.Tests.csproj", "{8F9E1322-568B-4F02-A1DD-4222C8457A42}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -118,6 +120,10 @@ Global {FE34728A-25AA-44E1-A3A6-AB500307C406}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE34728A-25AA-44E1-A3A6-AB500307C406}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE34728A-25AA-44E1-A3A6-AB500307C406}.Release|Any CPU.Build.0 = Release|Any CPU + {8F9E1322-568B-4F02-A1DD-4222C8457A42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F9E1322-568B-4F02-A1DD-4222C8457A42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F9E1322-568B-4F02-A1DD-4222C8457A42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F9E1322-568B-4F02-A1DD-4222C8457A42}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,6 +143,7 @@ Global {A7C96E1A-8CD6-4BFA-8128-9875B607BEF5} = {AEBE10B1-96B1-4060-B8C1-1F9BFA7A586C} {A85B6B1E-2030-47BE-9B9F-F645B08E501D} = {96857509-627A-4FD2-AC82-34387619A7B1} {FE34728A-25AA-44E1-A3A6-AB500307C406} = {96857509-627A-4FD2-AC82-34387619A7B1} + {8F9E1322-568B-4F02-A1DD-4222C8457A42} = {96857509-627A-4FD2-AC82-34387619A7B1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E1907BFD-C144-4B48-AA40-972F499D4E08} diff --git a/global.json b/global.json index 055ac3d4..76ce40d8 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "6.0.100", - "rollForward": "latestMinor", - "allowPrerelease": true + "rollForward": "latestMajor", + "allowPrerelease": false } } diff --git a/samples/AuthGettingStarted/Program.cs b/samples/AuthGettingStarted/Program.cs index 8a46a742..ecfb8981 100644 --- a/samples/AuthGettingStarted/Program.cs +++ b/samples/AuthGettingStarted/Program.cs @@ -2,6 +2,7 @@ using Api; using Keycloak.AuthServices.Authentication; using Keycloak.AuthServices.Authorization; +using Keycloak.AuthServices.Common; using Keycloak.AuthServices.Sdk.Admin; var builder = WebApplication.CreateBuilder(args); @@ -12,29 +13,32 @@ host.ConfigureLogger(); -services - .AddEndpointsApiExplorer() - .AddSwagger(); +services.AddEndpointsApiExplorer().AddSwagger(); var authenticationOptions = configuration .GetSection(KeycloakAuthenticationOptions.Section) - .Get(); + .Get(KeycloakInstallationOptions.KeycloakFormatBinder); -services.AddKeycloakAuthentication(authenticationOptions); +services.AddKeycloakAuthentication(authenticationOptions!); var authorizationOptions = configuration .GetSection(KeycloakProtectionClientOptions.Section) - .Get(); + .Get(KeycloakInstallationOptions.KeycloakFormatBinder); services - .AddAuthorization(o => o.AddPolicy("IsAdmin", b => - { - b.RequireRealmRoles("admin"); - b.RequireResourceRoles("r-admin"); - // TokenValidationParameters.RoleClaimType is overriden - // by KeycloakRolesClaimsTransformation - b.RequireRole("r-admin"); - })) + .AddAuthorization(o => + o.AddPolicy( + "IsAdmin", + b => + { + b.RequireRealmRoles("admin"); + b.RequireResourceRoles("r-admin"); + // TokenValidationParameters.RoleClaimType is overriden + // by KeycloakRolesClaimsTransformation + b.RequireRole("r-admin"); + } + ) + ) .AddKeycloakAuthorization(authorizationOptions); var adminClientOptions = configuration @@ -45,17 +49,19 @@ var app = builder.Build(); -app - .UseSwagger() - .UseSwaggerUI(); +app.UseSwagger().UseSwaggerUI(); app.UseAuthentication(); app.UseAuthorization(); -app.MapGet("/", (ClaimsPrincipal user) => -{ - // TokenValidationParameters.NameClaimType is overriden based on keycloak specific claim - app.Logger.LogInformation("{@User}", user.Identity.Name); -}).RequireAuthorization("IsAdmin"); +app.MapGet( + "/", + (ClaimsPrincipal user) => + { + // TokenValidationParameters.NameClaimType is overriden based on keycloak specific claim + app.Logger.LogInformation("{@User}", user.Identity.Name); + } + ) + .RequireAuthorization("IsAdmin"); app.Run(); diff --git a/samples/AuthZGettingStarted/ServiceCollectionExtensions.OpenApi.cs b/samples/AuthZGettingStarted/ServiceCollectionExtensions.OpenApi.cs index b6c7457c..9a4dd934 100644 --- a/samples/AuthZGettingStarted/ServiceCollectionExtensions.OpenApi.cs +++ b/samples/AuthZGettingStarted/ServiceCollectionExtensions.OpenApi.cs @@ -1,19 +1,21 @@ namespace Microsoft.Extensions.DependencyInjection; using Keycloak.AuthServices.Authentication; +using Keycloak.AuthServices.Common; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; public static partial class ServiceCollectionExtensions { - public static IServiceCollection AddApplicationSwagger(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddApplicationSwagger( + this IServiceCollection services, + IConfiguration configuration + ) { - KeycloakAuthenticationOptions options = new(); - - configuration + var options = configuration .GetSection(KeycloakAuthenticationOptions.Section) - .Bind(options, opt => opt.BindNonPublicProperties = true); + .Get(KeycloakInstallationOptions.KeycloakFormatBinder); services.AddEndpointsApiExplorer(); services.AddSwaggerGen(c => @@ -31,28 +33,34 @@ public static IServiceCollection AddApplicationSwagger(this IServiceCollection s { Implicit = new OpenApiOAuthFlow { - AuthorizationUrl = new Uri($"{options.KeycloakUrlRealm}/protocol/openid-connect/auth"), - TokenUrl = new Uri($"{options.KeycloakUrlRealm}/protocol/openid-connect/token"), + AuthorizationUrl = new Uri( + $"{options.KeycloakUrlRealm}/protocol/openid-connect/auth" + ), + TokenUrl = new Uri( + $"{options.KeycloakUrlRealm}/protocol/openid-connect/token" + ), Scopes = new Dictionary(), } } }; c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); - c.AddSecurityRequirement(new OpenApiSecurityRequirement - { - {securityScheme, Array.Empty()} - }); + c.AddSecurityRequirement( + new OpenApiSecurityRequirement { { securityScheme, Array.Empty() } } + ); }); return services; } - public static IApplicationBuilder UseApplicationSwagger(this IApplicationBuilder app, IConfiguration configuration) + public static IApplicationBuilder UseApplicationSwagger( + this IApplicationBuilder app, + IConfiguration configuration + ) { KeycloakAuthenticationOptions options = new(); configuration .GetSection(KeycloakAuthenticationOptions.Section) - .Bind(options, opt => opt.BindNonPublicProperties = true); + .Bind(options, KeycloakInstallationOptions.KeycloakFormatBinder); app.UseSwagger(); app.UseSwaggerUI(s => s.OAuthClientId(options.Resource)); diff --git a/samples/AuthorizationAndCleanArchitecture/AuthorizationAndCleanArchitecture.csproj b/samples/AuthorizationAndCleanArchitecture/AuthorizationAndCleanArchitecture.csproj index 84e4c95f..f9daf25e 100644 --- a/samples/AuthorizationAndCleanArchitecture/AuthorizationAndCleanArchitecture.csproj +++ b/samples/AuthorizationAndCleanArchitecture/AuthorizationAndCleanArchitecture.csproj @@ -20,7 +20,6 @@ - diff --git a/samples/Blazor/Server/Program.cs b/samples/Blazor/Server/Program.cs index f3854008..530e2089 100644 --- a/samples/Blazor/Server/Program.cs +++ b/samples/Blazor/Server/Program.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Keycloak.AuthServices.Authentication; using Microsoft.OpenApi.Models; using Serilog; @@ -40,7 +41,8 @@ .MinimumLevel.Verbose() .WriteTo.Console( outputTemplate: "[{Level:u4}] | {Message:lj}{NewLine}{Exception}", - restrictedToMinimumLevel: LogEventLevel.Information) + restrictedToMinimumLevel: LogEventLevel.Information, + formatProvider: CultureInfo.InvariantCulture) .CreateBootstrapLogger(); builder.Host.UseSerilog(); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f4734b54..2c2fffd7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,5 +1,11 @@ + + enable + enable + net6.0 + + Oleksii Nikiforov (@nikiforovall) https://github.com/NikiforovAll/keycloak-authorization-services-dotnet @@ -9,8 +15,8 @@ - preview normal + preview.0 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index aec97f9a..e3cc51a2 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,18 +1,22 @@ - - + + + + + + + + + + - - - - - + - - + + diff --git a/src/Keycloak.AuthServices.Authentication/Claims/.editorconfig b/src/Keycloak.AuthServices.Authentication/Claims/.editorconfig new file mode 100644 index 00000000..1886a1b1 --- /dev/null +++ b/src/Keycloak.AuthServices.Authentication/Claims/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +dotnet_diagnostic.CA1309.severity = none diff --git a/src/Keycloak.AuthServices.Authentication/Configuration/KeycloakConfigurationProvider.cs b/src/Keycloak.AuthServices.Authentication/Configuration/KeycloakConfigurationProvider.cs index f11f66b6..cf3e75a1 100644 --- a/src/Keycloak.AuthServices.Authentication/Configuration/KeycloakConfigurationProvider.cs +++ b/src/Keycloak.AuthServices.Authentication/Configuration/KeycloakConfigurationProvider.cs @@ -19,8 +19,8 @@ public class KeycloakConfigurationProvider : JsonConfigurationProvider /// Initializes a new instance with the specified source. /// /// The source settings. - public KeycloakConfigurationProvider(KeycloakConfigurationSource source) : base(source) => - this.stringBuilder = new StringBuilder(); + public KeycloakConfigurationProvider(KeycloakConfigurationSource source) + : base(source) => this.stringBuilder = new StringBuilder(); /// /// Loads the JSON data from a stream. @@ -32,7 +32,8 @@ public override void Load(Stream stream) this.Data = this.Data.ToDictionary( x => this.NormalizeKey(x.Key), x => x.Value, - StringComparer.OrdinalIgnoreCase); + StringComparer.OrdinalIgnoreCase + ); } /// @@ -42,8 +43,7 @@ public override void Load(Stream stream) /// private string NormalizeKey(string key) { - var sections = key - .ToUpper(CultureInfo.InvariantCulture) + var sections = key.ToUpper(CultureInfo.InvariantCulture) .Split(NestedConfigurationDelimiter); foreach (var section in sections) @@ -70,8 +70,10 @@ private string NormalizeKey(string key) } } - var result = ConfigurationConstants.ConfigurationPrefix + NestedConfigurationDelimiter + - this.stringBuilder.ToString(); + var result = + ConfigurationConstants.ConfigurationPrefix + + NestedConfigurationDelimiter + + this.stringBuilder.ToString(); this.stringBuilder.Clear(); return result; } diff --git a/src/Keycloak.AuthServices.Authentication/Keycloak.AuthServices.Authentication.csproj b/src/Keycloak.AuthServices.Authentication/Keycloak.AuthServices.Authentication.csproj index a80779ad..87ffd4c5 100644 --- a/src/Keycloak.AuthServices.Authentication/Keycloak.AuthServices.Authentication.csproj +++ b/src/Keycloak.AuthServices.Authentication/Keycloak.AuthServices.Authentication.csproj @@ -1,9 +1,6 @@ - net6.0 - enable - enable Keycloak.AuthServices.Authentication diff --git a/src/Keycloak.AuthServices.Authentication/ServiceCollectionExtensions.cs b/src/Keycloak.AuthServices.Authentication/ServiceCollectionExtensions.cs index 51bf98c8..9035b2e3 100644 --- a/src/Keycloak.AuthServices.Authentication/ServiceCollectionExtensions.cs +++ b/src/Keycloak.AuthServices.Authentication/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ namespace Keycloak.AuthServices.Authentication; using Claims; using Configuration; +using Keycloak.AuthServices.Common; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; @@ -20,7 +21,8 @@ public static class ServiceCollectionExtensions public static AuthenticationBuilder AddKeycloakAuthentication( this IServiceCollection services, KeycloakAuthenticationOptions keycloakOptions, - Action? configureOptions = default) + Action? configureOptions = default + ) { const string roleClaimType = "role"; var validationParameters = new TokenValidationParameters @@ -33,18 +35,22 @@ public static AuthenticationBuilder AddKeycloakAuthentication( }; // options.Resource == Audience - services.AddTransient(_ => - new KeycloakRolesClaimsTransformation( - roleClaimType, - keycloakOptions.RolesSource, - keycloakOptions.Resource)); + services.AddTransient(_ => new KeycloakRolesClaimsTransformation( + roleClaimType, + keycloakOptions.RolesSource, + keycloakOptions.Resource + )); - return services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + return services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opts => { - var sslRequired = string.IsNullOrWhiteSpace(keycloakOptions.SslRequired) - || keycloakOptions.SslRequired - .Equals("external", StringComparison.OrdinalIgnoreCase); + var sslRequired = + string.IsNullOrWhiteSpace(keycloakOptions.SslRequired) + || keycloakOptions.SslRequired.Equals( + "external", + StringComparison.OrdinalIgnoreCase + ); opts.Authority = keycloakOptions.KeycloakUrlRealm; opts.Audience = keycloakOptions.Resource; @@ -55,7 +61,6 @@ public static AuthenticationBuilder AddKeycloakAuthentication( }); } - /// /// Adds keycloak authentication services from configuration located in specified default section. /// @@ -66,15 +71,14 @@ public static AuthenticationBuilder AddKeycloakAuthentication( public static AuthenticationBuilder AddKeycloakAuthentication( this IServiceCollection services, IConfiguration configuration, - Action? configureOptions = default) + Action? configureOptions = default + ) { - KeycloakAuthenticationOptions options = new(); - - configuration + var authenticationOptions = configuration .GetSection(KeycloakAuthenticationOptions.Section) - .Bind(options, opt => opt.BindNonPublicProperties = true); + .Get(KeycloakInstallationOptions.KeycloakFormatBinder)!; - return services.AddKeycloakAuthentication(options, configureOptions); + return services.AddKeycloakAuthentication(authenticationOptions, configureOptions); } /// @@ -88,16 +92,15 @@ public static AuthenticationBuilder AddKeycloakAuthentication( public static AuthenticationBuilder AddKeycloakAuthentication( this IServiceCollection services, IConfiguration configuration, - string? keycloakClientSectionName, - Action? configureOptions = default) + string keycloakClientSectionName, + Action? configureOptions = default + ) { - KeycloakAuthenticationOptions options = new(); - - configuration - .GetSection(keycloakClientSectionName ?? KeycloakAuthenticationOptions.Section) - .Bind(options, opt => opt.BindNonPublicProperties = true); + var authenticationOptions = configuration + .GetSection(keycloakClientSectionName) + .Get(KeycloakInstallationOptions.KeycloakFormatBinder)!; - return services.AddKeycloakAuthentication(options, configureOptions); + return services.AddKeycloakAuthentication(authenticationOptions, configureOptions); } /// @@ -107,10 +110,14 @@ public static AuthenticationBuilder AddKeycloakAuthentication( /// /// public static IHostBuilder ConfigureKeycloakConfigurationSource( - this IHostBuilder hostBuilder, string fileName = "keycloak.json") => - hostBuilder.ConfigureAppConfiguration((_, builder) => - { - var source = new KeycloakConfigurationSource { Path = fileName, Optional = false }; - builder.Sources.Insert(0, source); - }); + this IHostBuilder hostBuilder, + string fileName = "keycloak.json" + ) => + hostBuilder.ConfigureAppConfiguration( + (_, builder) => + { + var source = new KeycloakConfigurationSource { Path = fileName, Optional = false }; + builder.Sources.Insert(0, source); + } + ); } diff --git a/src/Keycloak.AuthServices.Authorization/Keycloak.AuthServices.Authorization.csproj b/src/Keycloak.AuthServices.Authorization/Keycloak.AuthServices.Authorization.csproj index b87cb897..2d62d301 100644 --- a/src/Keycloak.AuthServices.Authorization/Keycloak.AuthServices.Authorization.csproj +++ b/src/Keycloak.AuthServices.Authorization/Keycloak.AuthServices.Authorization.csproj @@ -1,9 +1,6 @@ - net6.0 - enable - enable Keycloak.AuthServices.Authorization diff --git a/src/Keycloak.AuthServices.Authorization/ServiceCollectionExtensions.cs b/src/Keycloak.AuthServices.Authorization/ServiceCollectionExtensions.cs index 848b0244..18f65eb2 100644 --- a/src/Keycloak.AuthServices.Authorization/ServiceCollectionExtensions.cs +++ b/src/Keycloak.AuthServices.Authorization/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ namespace Keycloak.AuthServices.Authorization; +using Keycloak.AuthServices.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -17,7 +18,8 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddKeycloakAuthorization( this IServiceCollection services, - KeycloakProtectionClientOptions keycloakOptions) + KeycloakProtectionClientOptions keycloakOptions + ) { services.AddKeycloakProtectionHttpClient(keycloakOptions); @@ -40,13 +42,12 @@ public static IServiceCollection AddKeycloakAuthorization( public static IServiceCollection AddKeycloakAuthorization( this IServiceCollection services, IConfiguration configuration, - string? keycloakClientSectionName = default) + string? keycloakClientSectionName = default + ) { - KeycloakProtectionClientOptions options = new(); - - configuration + var options = configuration .GetSection(keycloakClientSectionName ?? KeycloakProtectionClientOptions.Section) - .Bind(options, opt => opt.BindNonPublicProperties = true); + .Get(KeycloakInstallationOptions.KeycloakFormatBinder)!; services.AddKeycloakAuthorization(options); return services; diff --git a/src/Keycloak.AuthServices.Common/Keycloak.AuthServices.Common.csproj b/src/Keycloak.AuthServices.Common/Keycloak.AuthServices.Common.csproj index 8f9046a6..45ae4451 100644 --- a/src/Keycloak.AuthServices.Common/Keycloak.AuthServices.Common.csproj +++ b/src/Keycloak.AuthServices.Common/Keycloak.AuthServices.Common.csproj @@ -1,10 +1,7 @@ - enable - enable Keycloak.AuthServices.Common - net6.0 @@ -15,6 +12,7 @@ + diff --git a/src/Keycloak.AuthServices.Common/KeycloakConstants.cs b/src/Keycloak.AuthServices.Common/KeycloakConstants.cs index 1fd23efc..5bbb1bfe 100644 --- a/src/Keycloak.AuthServices.Common/KeycloakConstants.cs +++ b/src/Keycloak.AuthServices.Common/KeycloakConstants.cs @@ -11,12 +11,12 @@ public static class KeycloakConstants public const string TokenEndpointPath = "protocol/openid-connect/token"; /// - /// JWT Token Claim - Resource Access + /// Token Claim - Resource Access /// public const string ResourceAccessClaimType = "resource_access"; /// - /// JWT Token Claim - Realm Access + /// Token Claim - Realm Access /// public const string RealmAccessClaimType = "realm_access"; diff --git a/src/Keycloak.AuthServices.Common/KeycloakInstallationOptions.cs b/src/Keycloak.AuthServices.Common/KeycloakInstallationOptions.cs index ea09b8c2..76ab4f62 100644 --- a/src/Keycloak.AuthServices.Common/KeycloakInstallationOptions.cs +++ b/src/Keycloak.AuthServices.Common/KeycloakInstallationOptions.cs @@ -21,14 +21,14 @@ public class KeycloakInstallationOptions /// /// "auth-server-url": "http://localhost:8088/auth/" /// - [ConfigurationKeyName("auth-server-url")] + [ConfigurationKeyName("AuthServerUrl")] public string AuthServerUrl { get => this.authServerUrl ?? this.AuthServerUrl2; set => this.authServerUrl = value; } - [ConfigurationKeyName("AuthServerUrl")] + [ConfigurationKeyName("auth-server-url")] private string AuthServerUrl2 { get; set; } = default!; /// @@ -47,13 +47,14 @@ public string AuthServerUrl /// /// Audience verification /// - [ConfigurationKeyName("verify-token-audience")] + [ConfigurationKeyName("VerifyTokenAudience")] public bool? VerifyTokenAudience { get => this.verifyTokenAudience ?? this.VerifyTokenAudience2; set => this.verifyTokenAudience = value; } - [ConfigurationKeyName("VerifyTokenAudience")] + + [ConfigurationKeyName("verify-token-audience")] private bool? VerifyTokenAudience2 { get; set; } /// @@ -67,25 +68,27 @@ public bool? VerifyTokenAudience /// /// - Default: 0 seconds /// - [ConfigurationKeyName("token-clock-skew")] + [ConfigurationKeyName("TokenClockSkew")] public TimeSpan TokenClockSkew { get => this.tokenClockSkew ?? this.TokenClockSkew2 ?? TimeSpan.Zero; set => this.tokenClockSkew = value; } - [ConfigurationKeyName("TokenClockSkew")] + + [ConfigurationKeyName("token-clock-skew")] private TimeSpan? TokenClockSkew2 { get; set; } /// /// Require HTTPS /// - [ConfigurationKeyName("ssl-required")] + [ConfigurationKeyName("SslRequired")] public string SslRequired { get => this.sslRequired ?? this.SslRequired2; set => this.sslRequired = value; } - [ConfigurationKeyName("SslRequired")] + + [ConfigurationKeyName("ssl-required")] private string SslRequired2 { get; set; } = default!; /// @@ -104,7 +107,16 @@ private static string NormalizeUrl(string url) /// RolesClaimTransformationSource /// [ConfigurationKeyName("RolesSource")] - public RolesClaimTransformationSource RolesSource { get; set; } = RolesClaimTransformationSource.ResourceAccess; + public RolesClaimTransformationSource RolesSource { get; set; } = + RolesClaimTransformationSource.ResourceAccess; + + /// + /// Default binder required to handle kebab-case configuration format used by Keycloak + /// +#pragma warning disable CA2211 // Non-constant fields should not be visible + public static Action KeycloakFormatBinder = options => + options.BindNonPublicProperties = true; +#pragma warning restore CA2211 // Non-constant fields should not be visible } /// @@ -112,8 +124,19 @@ private static string NormalizeUrl(string url) /// public enum RolesClaimTransformationSource { + /// + /// Specifies that no transformation should be applied from the source. + /// None, + + /// + /// Specifies that transformation should be applied to the realm. + /// Realm, + + /// + /// Specifies that transformation should be applied to the resource access. + /// ResourceAccess } diff --git a/src/Keycloak.AuthServices.Common/KeycloakResourceClaimsExtensions.cs b/src/Keycloak.AuthServices.Common/KeycloakResourceClaimsExtensions.cs index 64a95cf8..402a442a 100644 --- a/src/Keycloak.AuthServices.Common/KeycloakResourceClaimsExtensions.cs +++ b/src/Keycloak.AuthServices.Common/KeycloakResourceClaimsExtensions.cs @@ -3,27 +3,30 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.Json; +using static Keycloak.AuthServices.Common.KeycloakConstants; /// -/// Utils +/// Keycloak resource claims extensions. /// public static class KeycloakResourceClaimsExtensions { private const string ClaimValueType = "JSON"; /// - /// Try get claim from JWT + /// Tries to get the resource collection from the JWT claims. /// - /// - /// - /// + /// The JWT claims. + /// The resource access collection if found, otherwise null. + /// True if the resource collection was found, false otherwise. public static bool TryGetResourceCollection( this IEnumerable claims, - [MaybeNullWhen(false)] out ResourceAccessCollection resourceAccessCollection) + [MaybeNullWhen(false)] out ResourceAccessCollection resourceAccessCollection + ) { var claim = claims.SingleOrDefault(x => - x.Type.Equals(KeycloakConstants.ResourceAccessClaimType, StringComparison.OrdinalIgnoreCase) - && x.ValueType.Equals(ClaimValueType, StringComparison.OrdinalIgnoreCase)); + x.Type.Equals(ResourceAccessClaimType, StringComparison.OrdinalIgnoreCase) + && x.ValueType.Equals(ClaimValueType, StringComparison.OrdinalIgnoreCase) + ); if (claim == null || string.IsNullOrEmpty(claim.Value)) { @@ -38,18 +41,20 @@ public static bool TryGetResourceCollection( } /// - /// Try get Claim from JWT + /// Tries to get the realm resource from the JWT claims. /// - /// - /// - /// + /// The JWT claims. + /// The realm resource access if found, otherwise null. + /// True if the realm resource was found, false otherwise. public static bool TryGetRealmResource( this IEnumerable claims, - [MaybeNullWhen(false)] out ResourceAccess resourcesAccess) + [MaybeNullWhen(false)] out ResourceAccess resourcesAccess + ) { var claim = claims.SingleOrDefault(x => - x.Type.Equals(KeycloakConstants.RealmAccessClaimType, StringComparison.OrdinalIgnoreCase) - && x.ValueType.Equals(ClaimValueType, StringComparison.OrdinalIgnoreCase)); + x.Type.Equals(RealmAccessClaimType, StringComparison.OrdinalIgnoreCase) + && x.ValueType.Equals(ClaimValueType, StringComparison.OrdinalIgnoreCase) + ); if (claim == null || string.IsNullOrEmpty(claim.Value)) { @@ -58,6 +63,7 @@ public static bool TryGetRealmResource( } resourcesAccess = JsonSerializer.Deserialize(claim.Value)!; + return true; } } diff --git a/src/Keycloak.AuthServices.Common/ResourceAccessCollection.cs b/src/Keycloak.AuthServices.Common/ResourceAccessCollection.cs index b4d7c4e4..e4e8e50f 100644 --- a/src/Keycloak.AuthServices.Common/ResourceAccessCollection.cs +++ b/src/Keycloak.AuthServices.Common/ResourceAccessCollection.cs @@ -7,11 +7,10 @@ public class ResourceAccessCollection : Dictionary { /// + /// Initializes a new instance of the class. /// public ResourceAccessCollection() - : base(StringComparer.OrdinalIgnoreCase) - { - } + : base(StringComparer.OrdinalIgnoreCase) { } } /// @@ -19,8 +18,8 @@ public ResourceAccessCollection() public class ResourceAccess { /// + /// Represents a collection of resource access roles. /// [JsonPropertyName("roles")] public List Roles { get; init; } = new(); } - diff --git a/src/Keycloak.AuthServices.Sdk/Admin/Constants/KeycloakClientApiConstants.cs b/src/Keycloak.AuthServices.Sdk/Admin/Constants/KeycloakClientApiConstants.cs index e28fe310..e6cf01b4 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/Constants/KeycloakClientApiConstants.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/Constants/KeycloakClientApiConstants.cs @@ -11,7 +11,6 @@ internal static class KeycloakClientApiConstants internal const string GetRealm = $"{AdminApiBase}/{Realms}/{{realm}}"; - #region Resource API internal const string GetResources = "/realms/{realm}/authz/protection/resource_set"; @@ -22,7 +21,8 @@ internal static class KeycloakClientApiConstants internal const string PutResource = $"{GetResource}"; - internal const string GetResourceByExactName = "/realms/{realm}/authz/protection/resource_set?&exactName=true"; + internal const string GetResourceByExactName = + "/realms/{realm}/authz/protection/resource_set?&exactName=true"; #endregion @@ -43,8 +43,6 @@ internal static class KeycloakClientApiConstants internal const string ExecuteActionsEmail = $"{GetRealm}/users/{{id}}/execute-actions-email"; internal const string GetUserGroups = $"{GetRealm}/users/{{id}}/groups"; - - internal const string GetGroups = $"{GetRealm}/groups"; internal const string CreateGroup = $"{GetRealm}/groups"; diff --git a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakClient.cs b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakClient.cs index cfeeb70e..d880aafb 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakClient.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakClient.cs @@ -7,6 +7,7 @@ namespace Keycloak.AuthServices.Sdk.Admin; /// Aggregates multiple clients. and /// public interface IKeycloakClient - : IKeycloakRealmClient, IKeycloakProtectedResourceClient, IKeycloakUserClient, IKeycloakGroupClient -{ -} + : IKeycloakRealmClient, + IKeycloakProtectedResourceClient, + IKeycloakUserClient, + IKeycloakGroupClient { } diff --git a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakGroupClient.cs b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakGroupClient.cs index c81bb985..0b2cd0c0 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakGroupClient.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakGroupClient.cs @@ -12,7 +12,6 @@ [Headers("Accept: application/json")] public interface IKeycloakGroupClient { - /// /// Get a stream of groups on the realm. /// @@ -20,7 +19,10 @@ public interface IKeycloakGroupClient /// Optional query parameters. /// A stream of groups, filtered according to query parameters. [Get(KeycloakClientApiConstants.GetGroups)] - Task> GetGroups(string realm, [Query] GetGroupRequestParameters? parameters = default); + Task> GetGroups( + string realm, + [Query] GetGroupRequestParameters? parameters = default + ); /// /// Get representation of a group. @@ -53,6 +55,9 @@ public interface IKeycloakGroupClient /// [Put(KeycloakClientApiConstants.UpdateGroup)] [Headers("Content-Type: application/json")] - Task UpdateGroup(string realm, [AliasAs("id")] string groupId, [Body] Group group); - + Task UpdateGroup( + string realm, + [AliasAs("id")] string groupId, + [Body] Group group + ); } diff --git a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs index cd83c1d9..d8c9efbb 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/IKeycloakUserClient.cs @@ -19,7 +19,10 @@ public interface IKeycloakUserClient /// Optional query parameters. /// A stream of users, filtered according to query parameters. [Get(KeycloakClientApiConstants.GetUsers)] - Task> GetUsers(string realm, [Query] GetUsersRequestParameters? parameters = default); + Task> GetUsers( + string realm, + [Query] GetUsersRequestParameters? parameters = default + ); /// /// Get representation of a user. @@ -75,9 +78,12 @@ public interface IKeycloakUserClient /// Client ID. /// Redirect URI. The default for the redirect is the account client. [Put(KeycloakClientApiConstants.SendVerifyEmail)] - Task SendVerifyEmail(string realm, [AliasAs("id")] string userId, - [Query][AliasAs("client_id")] string? clientId = default, - [Query][AliasAs("redirect_uri")] string? redirectUri = default); + Task SendVerifyEmail( + string realm, + [AliasAs("id")] string userId, + [Query] [AliasAs("client_id")] string? clientId = default, + [Query] [AliasAs("redirect_uri")] string? redirectUri = default + ); /// /// Execute actions email for the user. @@ -90,11 +96,14 @@ Task SendVerifyEmail(string realm, [AliasAs("id")] string userId, /// Actions [Put(KeycloakClientApiConstants.ExecuteActionsEmail)] [Headers("Content-Type: application/json")] - Task ExecuteActionsEmail(string realm, [AliasAs("id")] string userId, - [Query][AliasAs("client_id")] string? clientId = default, + Task ExecuteActionsEmail( + string realm, + [AliasAs("id")] string userId, + [Query] [AliasAs("client_id")] string? clientId = default, [Query] int? lifespan = default, - [Query][AliasAs("redirect_uri")] string? redirectUri = default, - [Body] List? actions = default); + [Query] [AliasAs("redirect_uri")] string? redirectUri = default, + [Body] List? actions = default + ); /// /// Get a users's groups. @@ -104,5 +113,9 @@ Task ExecuteActionsEmail(string realm, [AliasAs("id")] string userId, /// Optional query parameters. /// A stream of users, filtered according to query parameters. [Get(KeycloakClientApiConstants.GetUserGroups)] - Task> GetUserGroups(string realm, [AliasAs("id")] string userId, [Query] GetGroupRequestParameters? parameters = default); + Task> GetUserGroups( + string realm, + [AliasAs("id")] string userId, + [Query] GetGroupRequestParameters? parameters = default + ); } diff --git a/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/Resource.cs b/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/Resource.cs index 2fca272f..93794538 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/Resource.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/Resource.cs @@ -36,7 +36,8 @@ public Resource(string name, string[] scopes) /// /// Resource scopes /// - [JsonPropertyName("resource_scopes")] public string[] Scopes { get; } + [JsonPropertyName("resource_scopes")] + public string[] Scopes { get; } /// /// Resource attributes diff --git a/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/ResourceResponse.cs b/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/ResourceResponse.cs index c640e775..2ec13c73 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/ResourceResponse.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/Models/Resources/ResourceResponse.cs @@ -16,6 +16,7 @@ public class ResourceResponse public string DisplayName { get; set; } public Dictionary> Attributes { get; set; } public List Uris { get; set; } + [JsonPropertyName("resource_scopes")] public List ResourceScopes { get; set; } public List Scopes { get; set; } diff --git a/src/Keycloak.AuthServices.Sdk/Admin/Requests/Groups/GetGroupRequestParameters.cs b/src/Keycloak.AuthServices.Sdk/Admin/Requests/Groups/GetGroupRequestParameters.cs index 17637e92..487ae6ab 100644 --- a/src/Keycloak.AuthServices.Sdk/Admin/Requests/Groups/GetGroupRequestParameters.cs +++ b/src/Keycloak.AuthServices.Sdk/Admin/Requests/Groups/GetGroupRequestParameters.cs @@ -1,10 +1,9 @@ namespace Keycloak.AuthServices.Sdk.Admin.Requests.Groups; -using Keycloak.AuthServices.Sdk.Admin.Models; using Refit; /// -/// Optional request parameters for the endpoint. +/// Optional request parameters for the GetGroups endpoint. /// It can be called in three different ways. /// /// @@ -14,17 +13,17 @@ namespace Keycloak.AuthServices.Sdk.Admin.Requests.Groups; /// /// /// -/// If is specified, other criteria such as +/// If is specified, other criteria such as LastName /// will be ignored even though you may set them. The string will be matched against -/// the , , -/// and the of a . +/// the User.FirstName, User.LastName, User.Username +/// and the User.Email of a User/>. /// /// /// /// -/// If is unspecified but any of , , -/// or are specified, then those criteria are matched against -/// their respective fields on a entity. Combined with a logical AND. +/// If is unspecified but any of LastName, FirstName, +/// Email or Username are specified, then those criteria are matched against +/// their respective fields on a User entity. Combined with a logical AND. /// /// /// @@ -38,8 +37,8 @@ public class GetGroupRequestParameters public bool? BriefRepresentation { get; init; } /// - /// Defines whether the params , , - /// and must match exactly + /// Defines whether the params LastName, FirstName, + /// Email and Username must match exactly /// [AliasAs("exact")] public bool? Exact { get; init; } @@ -63,8 +62,8 @@ public class GetGroupRequestParameters public string? Query { get; init; } /// - /// Search for a string contained in , , - /// or . + /// Search for a string contained in Username, FirstName, + /// LastName or Email. /// [AliasAs("search")] public string? Search { get; init; } diff --git a/src/Keycloak.AuthServices.Sdk/AuthZ/IKeycloakProtectionClient.cs b/src/Keycloak.AuthServices.Sdk/AuthZ/IKeycloakProtectionClient.cs index dd21e001..47e2273c 100644 --- a/src/Keycloak.AuthServices.Sdk/AuthZ/IKeycloakProtectionClient.cs +++ b/src/Keycloak.AuthServices.Sdk/AuthZ/IKeycloakProtectionClient.cs @@ -12,5 +12,9 @@ public interface IKeycloakProtectionClient /// /// /// - Task VerifyAccessToResource(string resource, string scope, CancellationToken cancellationToken); + Task VerifyAccessToResource( + string resource, + string scope, + CancellationToken cancellationToken + ); } diff --git a/src/Keycloak.AuthServices.Sdk/AuthZ/KeycloakProtectionClient.cs b/src/Keycloak.AuthServices.Sdk/AuthZ/KeycloakProtectionClient.cs index d2ef1b76..2b6caf4c 100644 --- a/src/Keycloak.AuthServices.Sdk/AuthZ/KeycloakProtectionClient.cs +++ b/src/Keycloak.AuthServices.Sdk/AuthZ/KeycloakProtectionClient.cs @@ -16,7 +16,9 @@ public class KeycloakProtectionClient : IKeycloakProtectionClient /// /// public KeycloakProtectionClient( - HttpClient httpClient, KeycloakProtectionClientOptions clientOptions) + HttpClient httpClient, + KeycloakProtectionClientOptions clientOptions + ) { this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); this.clientOptions = clientOptions; @@ -24,22 +26,26 @@ public KeycloakProtectionClient( /// public async Task VerifyAccessToResource( - string resource, string scope, CancellationToken cancellationToken) + string resource, + string scope, + CancellationToken cancellationToken + ) { var audience = this.clientOptions.Resource; var data = new Dictionary { - {"grant_type", "urn:ietf:params:oauth:grant-type:uma-ticket"}, - {"response_mode", "decision"}, - {"audience", audience ?? string.Empty}, - {"permission", $"{resource}#{scope}"} + { "grant_type", "urn:ietf:params:oauth:grant-type:uma-ticket" }, + { "response_mode", "decision" }, + { "audience", audience ?? string.Empty }, + { "permission", $"{resource}#{scope}" } }; - // client.DefaultRequestHeaders.Authorization = new("Bearer", token); - // var configuration = await options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None); var response = await this.httpClient.PostAsync( - KeycloakConstants.TokenEndpointPath, new FormUrlEncodedContent(data), cancellationToken); + KeycloakConstants.TokenEndpointPath, + new FormUrlEncodedContent(data), + cancellationToken + ); return response.IsSuccessStatusCode; } diff --git a/src/Keycloak.AuthServices.Sdk/AuthZ/ServiceCollectionExtensions.cs b/src/Keycloak.AuthServices.Sdk/AuthZ/ServiceCollectionExtensions.cs index c2883518..8d603015 100644 --- a/src/Keycloak.AuthServices.Sdk/AuthZ/ServiceCollectionExtensions.cs +++ b/src/Keycloak.AuthServices.Sdk/AuthZ/ServiceCollectionExtensions.cs @@ -19,17 +19,20 @@ public static class ServiceCollectionExtensions public static IHttpClientBuilder AddKeycloakProtectionHttpClient( this IServiceCollection services, KeycloakProtectionClientOptions keycloakOptions, - Action? configureClient = default) + Action? configureClient = default + ) { services.AddSingleton(keycloakOptions); services.AddHttpContextAccessor(); - return services.AddHttpClient() + return services + .AddHttpClient() .ConfigureHttpClient(client => { var baseUrl = new Uri(keycloakOptions.KeycloakUrlRealm.TrimEnd('/') + "/"); client.BaseAddress = baseUrl; configureClient?.Invoke(client); - }).AddHeaderPropagation(); + }) + .AddHeaderPropagation(); } } diff --git a/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationExtensions.cs b/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationExtensions.cs index ac37e3d2..82fa77c4 100644 --- a/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationExtensions.cs +++ b/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationExtensions.cs @@ -15,12 +15,14 @@ public static class AccessTokenPropagationExtensions /// public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder builder) { - builder.AddHttpMessageHandler((sp) => - { - var contextAccessor = sp.GetRequiredService(); + builder.AddHttpMessageHandler( + (sp) => + { + var contextAccessor = sp.GetRequiredService(); - return new AccessTokenPropagationHandler(contextAccessor); - }); + return new AccessTokenPropagationHandler(contextAccessor); + } + ); return builder; } diff --git a/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationHandler.cs b/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationHandler.cs index 46c9a39a..be0d9bd4 100644 --- a/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationHandler.cs +++ b/src/Keycloak.AuthServices.Sdk/HttpMiddleware/AccessTokenPropagationHandler.cs @@ -17,14 +17,14 @@ public class AccessTokenPropagationHandler : DelegatingHandler /// Constructs /// /// - public AccessTokenPropagationHandler(IHttpContextAccessor contextAccessor) - { + public AccessTokenPropagationHandler(IHttpContextAccessor contextAccessor) => this.contextAccessor = contextAccessor; - } /// protected override async Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken) + HttpRequestMessage request, + CancellationToken cancellationToken + ) { if (this.contextAccessor.HttpContext == null) { @@ -32,8 +32,10 @@ protected override async Task SendAsync( } var httpContext = this.contextAccessor.HttpContext; - var token = await httpContext - .GetTokenAsync(JwtBearerDefaults.AuthenticationScheme, "access_token"); + var token = await httpContext.GetTokenAsync( + JwtBearerDefaults.AuthenticationScheme, + "access_token" + ); if (!StringValues.IsNullOrEmpty(token)) { @@ -42,7 +44,6 @@ protected override async Task SendAsync( return await Continue(); - Task Continue() => - base.SendAsync(request, cancellationToken); + Task Continue() => base.SendAsync(request, cancellationToken); } } diff --git a/src/Keycloak.AuthServices.Sdk/Keycloak.AuthServices.Sdk.csproj b/src/Keycloak.AuthServices.Sdk/Keycloak.AuthServices.Sdk.csproj index 61f0c5e5..418e979e 100644 --- a/src/Keycloak.AuthServices.Sdk/Keycloak.AuthServices.Sdk.csproj +++ b/src/Keycloak.AuthServices.Sdk/Keycloak.AuthServices.Sdk.csproj @@ -1,9 +1,6 @@ - net6.0 - enable - enable Keycloak.AuthServices.Sdk diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 14a3f03c..6f158ef8 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,4 +1,12 @@ + + + net6.0 + enable + enable + false + + diff --git a/tests/Keycloak.AuthServices.Authentication.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs b/tests/Keycloak.AuthServices.Authentication.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs index f0530ed3..4fd67561 100644 --- a/tests/Keycloak.AuthServices.Authentication.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs +++ b/tests/Keycloak.AuthServices.Authentication.Tests/Claims/KeycloakRolesClaimsTransformationTests.cs @@ -19,7 +19,7 @@ public async Task ClaimsTransformationShouldMap(RolesClaimTransformationSource r var claimsPrincipal = GetClaimsPrincipal(); // This should work many times - for(var testCount = 0; testCount < 3; testCount++) + for (var testCount = 0; testCount < 3; testCount++) { claimsPrincipal = await target.TransformAsync(claimsPrincipal); claimsPrincipal.HasClaim(ClaimTypes.Role, MyFirstClaim).Should().BeTrue(); @@ -30,11 +30,13 @@ public async Task ClaimsTransformationShouldMap(RolesClaimTransformationSource r // The resource_access claim type private const string MyResourceClaimType = "resource_access"; - private const string MyResourceClaimValue = @$"{{""{MyAudience}"":{{""roles"":[""{MyFirstClaim}"",""{MySecondClaim}""]}}}}"; + private const string MyResourceClaimValue = + @$"{{""{MyAudience}"":{{""roles"":[""{MyFirstClaim}"",""{MySecondClaim}""]}}}}"; // The realm_access claim type private const string MyRealmClaimType = "realm_access"; - private const string MyRealmClaimValue = @$"{{""roles"":[""{MyFirstClaim}"",""{MySecondClaim}""]}}"; + private const string MyRealmClaimValue = + @$"{{""roles"":[""{MyFirstClaim}"",""{MySecondClaim}""]}}"; // Fake claim values private const string MyFirstClaim = "my_client_app_role_user"; @@ -51,10 +53,13 @@ public async Task ClaimsTransformationShouldMap(RolesClaimTransformationSource r // Get a claims principal that has all the appropriate claim details required for testing private static ClaimsPrincipal GetClaimsPrincipal() => - new(new ClaimsIdentity(new[] - { - new Claim(MyResourceClaimType, MyResourceClaimValue, MyValueType, MyUrl, MyUrl), - new Claim(MyRealmClaimType, MyRealmClaimValue, MyValueType, MyUrl, MyUrl), - })); - + new( + new ClaimsIdentity( + new[] + { + new Claim(MyResourceClaimType, MyResourceClaimValue, MyValueType, MyUrl, MyUrl), + new Claim(MyRealmClaimType, MyRealmClaimValue, MyValueType, MyUrl, MyUrl), + } + ) + ); } diff --git a/tests/Keycloak.AuthServices.Authentication.Tests/Keycloak.AuthServices.Authentication.Tests.csproj b/tests/Keycloak.AuthServices.Authentication.Tests/Keycloak.AuthServices.Authentication.Tests.csproj index 02599842..53b93593 100644 --- a/tests/Keycloak.AuthServices.Authentication.Tests/Keycloak.AuthServices.Authentication.Tests.csproj +++ b/tests/Keycloak.AuthServices.Authentication.Tests/Keycloak.AuthServices.Authentication.Tests.csproj @@ -1,13 +1,5 @@ - - net6.0 - enable - enable - - false - - diff --git a/tests/Keycloak.AuthServices.Common.Tests/Keycloak.AuthServices.Common.Tests.csproj b/tests/Keycloak.AuthServices.Common.Tests/Keycloak.AuthServices.Common.Tests.csproj new file mode 100644 index 00000000..78798f83 --- /dev/null +++ b/tests/Keycloak.AuthServices.Common.Tests/Keycloak.AuthServices.Common.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + Always + + + + diff --git a/tests/Keycloak.AuthServices.Common.Tests/KeycloakInstallationOptionsTests.cs b/tests/Keycloak.AuthServices.Common.Tests/KeycloakInstallationOptionsTests.cs new file mode 100644 index 00000000..189d8677 --- /dev/null +++ b/tests/Keycloak.AuthServices.Common.Tests/KeycloakInstallationOptionsTests.cs @@ -0,0 +1,42 @@ +namespace Keycloak.AuthServices.Common.Tests; + +using FluentAssertions; +using Microsoft.Extensions.Configuration; + +public class KeycloakInstallationOptionsTests +{ + private static readonly KeycloakInstallationOptions Expected = + new() + { + Realm = "Test", + AuthServerUrl = "http://localhost:8080/", + SslRequired = "none", + Resource = "test-client", + VerifyTokenAudience = false, + Credentials = new() { Secret = "secret" }, + }; + + [Fact] + public void TestKebabCaseNotation() + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + var authenticationOptions = configuration + .GetSection("Keycloak1") + .Get(KeycloakInstallationOptions.KeycloakFormatBinder); + + authenticationOptions.Should().BeEquivalentTo(Expected); + } + + [Fact] + public void TestPascalCaseNotation() + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + var authenticationOptions = configuration + .GetSection("Keycloak2") + .Get(); + + authenticationOptions.Should().BeEquivalentTo(Expected); + } +} diff --git a/tests/Keycloak.AuthServices.Common.Tests/appsettings.json b/tests/Keycloak.AuthServices.Common.Tests/appsettings.json new file mode 100644 index 00000000..8bd97eee --- /dev/null +++ b/tests/Keycloak.AuthServices.Common.Tests/appsettings.json @@ -0,0 +1,22 @@ +{ + "Keycloak1": { + "realm": "Test", + "auth-server-url": "http://localhost:8080/", + "ssl-required": "none", + "resource": "test-client", + "verify-token-audience": false, + "credentials": { + "secret": "secret" + } + }, + "Keycloak2": { + "Realm": "Test", + "AuthServerUrl": "http://localhost:8080/", + "SslRequired": "none", + "Resource": "test-client", + "VerifyTokenAudience": false, + "Credentials": { + "Secret": "secret" + } + } +} diff --git a/tests/Keycloak.AuthServices.Sdk.Tests/Keycloak.AuthServices.Sdk.Tests.csproj b/tests/Keycloak.AuthServices.Sdk.Tests/Keycloak.AuthServices.Sdk.Tests.csproj index f55b1c20..381fa27b 100644 --- a/tests/Keycloak.AuthServices.Sdk.Tests/Keycloak.AuthServices.Sdk.Tests.csproj +++ b/tests/Keycloak.AuthServices.Sdk.Tests/Keycloak.AuthServices.Sdk.Tests.csproj @@ -1,11 +1,5 @@ - - net6.0 - enable - enable - -