Skip to content

Commit

Permalink
feat: refactor authorization package - add auth server (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll authored Apr 25, 2024
1 parent fe51770 commit 47d8f53
Show file tree
Hide file tree
Showing 27 changed files with 308 additions and 172 deletions.
10 changes: 10 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Key Changes in 2.0.0

* Moved `IKeycloakProtectionClient` to `Keycloak.AuthServices.Authorization`. Removed `AddKeycloakProtectionHttpClient`, added `AddAuthorizationServer` instead.

```csharp
// Before
.AddKeycloakAuthorization(configuration)
// After
.AddKeycloakAuthorization().AddAuthorizationServer(configuration)
```

* Dropped namespace `Keycloak.AuthServices.Sdk.AuthZ`
* `AddKeycloakAuthentication` has been deprecated in favor of `AddKeycloakWebApiAuthentication`.
* Breaking change 💥: Changed default Configuration format from kebab-case to PascalCase:

Expand Down
5 changes: 3 additions & 2 deletions samples/AuthGettingStarted/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
}
)
)
.AddKeycloakAuthorization(configuration);
.AddKeycloakAuthorization()
.AddAuthorizationServer(configuration);

services.AddKeycloakAdminHttpClient(configuration);

Expand All @@ -41,7 +42,7 @@
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", (ClaimsPrincipal user) => app.Logger.LogInformation("{@User}", user.Identity.Name))
app.MapGet("/", (ClaimsPrincipal user) => app.Logger.LogInformation("{@User}", user.Identity!.Name))

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-macOS-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-macOS-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-macOS-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-macOS-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 45 in samples/AuthGettingStarted/Program.cs

View workflow job for this annotation

GitHub Actions / Build-windows-latest

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)
.RequireAuthorization("IsAdmin");

app.Run();
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ namespace Api.Application.Authorization;
using Abstractions;
using MediatR;

public class AuthorizationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
public class AuthorizationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IIdentityService identityService;
private readonly ILogger<TRequest> logger;

public AuthorizationBehavior(IIdentityService identityService, ILogger<TRequest> logger)
{
this.identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
this.identityService =
identityService ?? throw new ArgumentNullException(nameof(identityService));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
CancellationToken cancellationToken
)
{
// TODO: consider reflection performance impact
var authorizeAttributes = request
Expand All @@ -42,7 +44,8 @@ public async Task<TResponse> Handle(

private async Task EnsureAuthorizedPolicies(
TRequest request,
IEnumerable<AuthorizeAttribute> authorizeAttributes)
IEnumerable<AuthorizeAttribute> authorizeAttributes
)
{
// Policy-based authorization
var authorizeAttributesWithPolicies = authorizeAttributes
Expand All @@ -54,28 +57,33 @@ private async Task EnsureAuthorizedPolicies(
return;
}

var requiredPolicies = authorizeAttributesWithPolicies
.Select(a =>
var requiredPolicies = authorizeAttributesWithPolicies.Select(a =>
{
if (
a is AuthorizeProtectedResourceAttribute resourceAttribute
&& request is IRequestWithResourceId requestWithResourceId
)
{
if (a is AuthorizeProtectedResourceAttribute resourceAttribute
&& request is IRequestWithResourceId requestWithResourceId)
{
resourceAttribute.ResourceId = requestWithResourceId.ResourceId;
}
return a.Policy;
});
resourceAttribute.ResourceId = requestWithResourceId.ResourceId;
}
return a.Policy;
});

foreach (var policy in requiredPolicies)
{
var authorized = await this.identityService
.AuthorizeAsync(this.identityService.Principal!, policy!);
var authorized = await this.identityService.AuthorizeAsync(
this.identityService.Principal!,
policy!
);

if (authorized)
{
continue;
}

#pragma warning disable CA1848 // Use the LoggerMessage delegates
this.logger.LogDebug("Failed policy authorization {Policy}", policy);
#pragma warning restore CA1848 // Use the LoggerMessage delegates
throw new ForbiddenAccessException();
}
}
Expand All @@ -96,12 +104,16 @@ private void EnsureAuthorizedRoles(IEnumerable<AuthorizeAttribute> authorizeAttr
.Where(a => !string.IsNullOrWhiteSpace(a.Roles))
.Select(a => a.Roles!.Split(','));

if (requiredRoles
.Select(roles => roles.Any(
role => this.identityService.IsInRoleAsync(role.Trim())))
.Any(authorized => !authorized))
if (
requiredRoles
.Select(roles => roles.Any(role => this.identityService.IsInRoleAsync(role.Trim())))
.Any(authorized => !authorized)
)
{
#pragma warning disable CA1848 // Use the LoggerMessage delegates
this.logger.LogDebug("Failed role authorization");
#pragma warning restore CA1848 // Use the LoggerMessage delegates

throw new ForbiddenAccessException();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ public static class ApplicationLogger
{
public static void ConfigureLogger(this ConfigureHostBuilder host)
{
#pragma warning disable CA1305 // Specify IFormatProvider
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Console(
outputTemplate: "[{Level:u4}] |{SourceContext,30}({EventId})| {Message:lj}{NewLine}{Exception}",
restrictedToMinimumLevel: LogEventLevel.Debug)
.CreateBootstrapLogger();
#pragma warning restore CA1305 // Specify IFormatProvider

host.UseSerilog();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Api.Controllers;

using Keycloak.AuthServices.Sdk.AuthZ;
using Keycloak.AuthServices.Authorization.AuthorizationServer;
using Microsoft.AspNetCore.Mvc;

[Route("api/authz")]
Expand Down
6 changes: 2 additions & 4 deletions samples/AuthorizationAndCleanArchitecture/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authentication.Configuration;
using Keycloak.AuthServices.Authorization;
using Keycloak.AuthServices.Sdk.Admin;
using Microsoft.AspNetCore.Authorization;


Expand Down Expand Up @@ -53,15 +52,14 @@
{
b.RequireResourceRoles("Manager");
});
}).AddKeycloakAuthorization(configuration);
}).AddKeycloakAuthorization()
.AddAuthorizationServer(configuration);

services.AddSingleton<IAuthorizationPolicyProvider, ProtectedResourcePolicyProvider>();

services.AddControllers(options =>
options.Filters.Add<ApiExceptionFilterAttribute>());

services.AddKeycloakAdminHttpClient(configuration);

var app = builder.Build();

app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static IServiceCollection AddAuth(this IServiceCollection services, IConf
.RequireAuthenticatedUser()
.RequireProtectedResource("workspace", "workspaces:read"));
}).AddKeycloakAuthorization(configuration);
}).AddKeycloakAuthorization().AddAuthorizationServer(configuration);

return services;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.29" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>

<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />


<PackageVersion Include="IdentityModel.AspNetCore" Version="4.3.0"/>
<PackageVersion Include="Refit.HttpClientFactory" Version="7.0.0"/>
<PackageVersion Include="Refit" Version="7.0.0"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace Keycloak.AuthServices.Sdk.HttpMiddleware;
namespace Keycloak.AuthServices.Authorization.AuthorizationServer;

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

/// <summary>
/// Token propagation middleware
Expand All @@ -19,8 +20,9 @@ public static IHttpClientBuilder AddHeaderPropagation(this IHttpClientBuilder bu
(sp) =>
{
var contextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
var options = sp.GetRequiredService<IOptions<KeycloakProtectionClientOptions>>();
return new AccessTokenPropagationHandler(contextAccessor);
return new AccessTokenPropagationHandler(contextAccessor, options);
}
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
namespace Keycloak.AuthServices.Sdk.HttpMiddleware;
namespace Keycloak.AuthServices.Authorization.AuthorizationServer;

using IdentityModel.Client;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.Options;

/// <summary>
/// Delegating handler to propagate headers
/// </summary>

public class AccessTokenPropagationHandler : DelegatingHandler
{
private readonly IHttpContextAccessor contextAccessor;
private readonly KeycloakProtectionClientOptions options;

/// <summary>
/// Constructs
/// Initializes a new instance of the <see cref="AccessTokenPropagationHandler"/> class.
/// </summary>
/// <param name="contextAccessor"></param>
public AccessTokenPropagationHandler(IHttpContextAccessor contextAccessor) =>
/// <param name="contextAccessor">The HTTP context accessor.</param>
/// <param name="options">The Keycloak protection client options.</param>
public AccessTokenPropagationHandler(
IHttpContextAccessor contextAccessor,
IOptions<KeycloakProtectionClientOptions> options
)
{
this.contextAccessor = contextAccessor;
this.options = options.Value;
}

/// <inheritdoc/>
protected override async Task<HttpResponseMessage> SendAsync(
Expand All @@ -32,14 +40,18 @@ CancellationToken cancellationToken
}

var httpContext = this.contextAccessor.HttpContext;

var token = await httpContext.GetTokenAsync(
JwtBearerDefaults.AuthenticationScheme,
this.options.SourceAuthenticationScheme,
"access_token"
);

if (!StringValues.IsNullOrEmpty(token))
if (!string.IsNullOrEmpty(token))
{
request.SetToken(JwtBearerDefaults.AuthenticationScheme, token);
request.Headers.Authorization = new AuthenticationHeaderValue(
this.options.SourceAuthenticationScheme,
token
);
}

return await Continue();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Keycloak.AuthServices.Sdk.AuthZ;
namespace Keycloak.AuthServices.Authorization.AuthorizationServer;

/// <summary>
/// Keycloak Protection API
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
namespace Keycloak.AuthServices.Sdk.AuthZ;
namespace Keycloak.AuthServices.Authorization.AuthorizationServer;

using Keycloak.AuthServices.Authorization;
using Keycloak.AuthServices.Common;
using Microsoft.Extensions.Options;

/// <inheritdoc />
public class KeycloakProtectionClient : IKeycloakProtectionClient
{
private readonly HttpClient httpClient;
private readonly KeycloakProtectionClientOptions clientOptions;
private readonly IOptions<KeycloakProtectionClientOptions> clientOptions;

/// <summary>
/// Constructs KeycloakProtectionClient
Expand All @@ -17,7 +17,7 @@ public class KeycloakProtectionClient : IKeycloakProtectionClient
/// <exception cref="ArgumentNullException"></exception>
public KeycloakProtectionClient(
HttpClient httpClient,
KeycloakProtectionClientOptions clientOptions
IOptions<KeycloakProtectionClientOptions> clientOptions
)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Expand All @@ -31,7 +31,7 @@ public async Task<bool> VerifyAccessToResource(
CancellationToken cancellationToken
)
{
var audience = this.clientOptions.Resource;
var audience = this.clientOptions.Value.Resource;

var data = new Dictionary<string, string>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Keycloak.AuthServices.Authorization.AuthorizationServer;

using Common;

/// <summary>
/// Defines a set of options used to perform Authorization Server calls
/// </summary>
public sealed class KeycloakProtectionClientOptions : KeycloakInstallationOptions
{
/// <summary>
/// Default section name.
/// </summary>
public const string Section = ConfigurationConstants.ConfigurationPrefix;

/// <summary>
/// Gets or sets the source authentication scheme used for header propagation.
/// </summary>
public string SourceAuthenticationScheme { get; set; } = "Bearer";

/// <summary>
/// Gets or sets a value indicating whether to use the protected resource policy provider.
/// </summary>
/// <remarks>
/// When set to true, the protected resource policy provider will be used to dynamically register policies based on their names.
/// </remarks>
public bool UseProtectedResourcePolicyProvider { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@
<PropertyGroup Label="Package">
<Product>Keycloak.AuthServices.Authorization</Product>
<Description>Keycloak Authorization Server integration. Decision + Rpt</Description>
<PackageTags>Keycloak;Authentication;Authrorization;authserver;JWT;OICD;jwt-authentication</PackageTags>
<PackageTags>Keycloak;Authrorization;authserver</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Keycloak.AuthServices.Common\Keycloak.AuthServices.Common.csproj" PrivateAssets="All" />
<ProjectReference Include="..\Keycloak.AuthServices.Sdk\Keycloak.AuthServices.Sdk.csproj" PrivateAssets="All" />
</ItemGroup>

</Project>
Loading

0 comments on commit 47d8f53

Please sign in to comment.