Skip to content

Commit

Permalink
feat: Add RBAC Authorization (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll authored May 1, 2024
1 parent d2d509a commit 9ba23f4
Show file tree
Hide file tree
Showing 37 changed files with 1,122 additions and 316 deletions.
7 changes: 7 additions & 0 deletions KeycloakAuthorizationServicesDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Integ
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebApi", "tests\TestWebApi\TestWebApi.csproj", "{0F40EFE2-8D17-46B2-A91B-EC4BCB93E77C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Authorization.Tests", "tests\Keycloak.AuthServices.Authorization.Tests\Keycloak.AuthServices.Authorization.Tests.csproj", "{331F4EF5-9CFE-4060-B903-69CCE9062BFD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -142,6 +144,10 @@ Global
{0F40EFE2-8D17-46B2-A91B-EC4BCB93E77C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F40EFE2-8D17-46B2-A91B-EC4BCB93E77C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F40EFE2-8D17-46B2-A91B-EC4BCB93E77C}.Release|Any CPU.Build.0 = Release|Any CPU
{331F4EF5-9CFE-4060-B903-69CCE9062BFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{331F4EF5-9CFE-4060-B903-69CCE9062BFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{331F4EF5-9CFE-4060-B903-69CCE9062BFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{331F4EF5-9CFE-4060-B903-69CCE9062BFD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -165,6 +171,7 @@ Global
{671BA3B1-DBF2-4161-97B5-433B91A3730E} = {AEBE10B1-96B1-4060-B8C1-1F9BFA7A586C}
{7499F9F0-1132-46B4-AAA2-D60D9F113293} = {96857509-627A-4FD2-AC82-34387619A7B1}
{0F40EFE2-8D17-46B2-A91B-EC4BCB93E77C} = {96857509-627A-4FD2-AC82-34387619A7B1}
{331F4EF5-9CFE-4060-B903-69CCE9062BFD} = {96857509-627A-4FD2-AC82-34387619A7B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E1907BFD-C144-4B48-AA40-972F499D4E08}
Expand Down
96 changes: 4 additions & 92 deletions docs/configuration/configuration-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ Not everything you want to do can be configured with `KeycloakAuthenticationOpti

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides

Here is a trick to bind options from configuration an override directly in the same code:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides2{3-5}

Typically, ASP.NET Core expects to find these (default) options under the `Authentication:Schemes:{SchemeName}`. See [Configuring Authentication **Strategies**](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security?view=aspnetcore-8.0#configuring-authentication-strategy) for more details. Here is how to configure [JwtBearerOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions):

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
Expand Down Expand Up @@ -195,95 +199,3 @@ Here is an example of **keycloak.json** adapter file:

```

## Keycloak Claims Transformation

Keycloak roles can be automatically transformed to [AspNetCore Roles](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles). This feature is disabled by default.

Specify `KeycloakAuthenticationOptions.RolesSource` to enable it. E.g.:

```json
{
"Keycloak": {
"RolesSource": "Realm"
}
}
```

There are three options to determine a source for the roles:

```csharp
public enum RolesClaimTransformationSource
{
/// <summary>
/// No Transformation. Default
/// </summary>
None,

/// <summary>
/// Use realm roles as source
/// </summary>
Realm,

/// <summary>
/// Use client roles as source
/// </summary>
ResourceAccess
}
```

Here is an example of decoded JWT token:

```json
{
"exp": 1714057504,
"iat": 1714057204,
"jti": "7250d2a9-e5a1-442f-9e76-5e6b78bb2760",
"iss": "http://localhost:8080/realms/Test",
"aud": [
"test-client",
"account"
],
"sub": "bf0b3371-ccdc-44f6-8861-ce25cbfcac39",
"typ": "Bearer",
"azp": "test-client",
"session_state": "563332d2-111a-4ef2-b6a0-ebc1d3ae9a1e",
"acr": "1",
"allowed-origins": [
"/*"
],
"realm_access": {
"roles": [
"default-roles-test",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "profile email",
"sid": "563332d2-111a-4ef2-b6a0-ebc1d3ae9a1e",
"email_verified": false,
"name": "Test Test",
"preferred_username": "test",
"given_name": "Test",
"family_name": "Test",
"email": "[email protected]"
}
```

If we specify `KeycloakAuthenticationOptions.RolesSource = RolesClaimTransformationSource.Realm` the roles are taken from $token.realm_access.roles.

Result = ["default-roles-test","offline_access","uma_authorization"]

If we specify `KeycloakAuthenticationOptions.RolesSource = RolesClaimTransformationSource.ResourceAccess` and `KeycloakAuthenticationOptions.RolesResource="account"` the roles are taken from $token.realm_access.account.roles.

Result = ["manage-account","manage-account-links","view-profile"]

The target claim can be configured `KeycloakAuthenticationOptions.RoleClaimType`, the default value is "role".
119 changes: 118 additions & 1 deletion docs/configuration/configuration-authorization.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,120 @@
# Configure Authorization

🚧👋 Come back later
*RBAC* (Role-Based Access Control) is a widely used authorization model in software applications. It provides a way to control access to resources based on the roles assigned to users. Keycloak, an open-source identity and access management solution, offers robust support for RBAC.

With Keycloak, you can configure roles by defining realm roles and resource roles. Realm roles are global roles that apply to the entire realm, while resource roles are specific to a particular client or resource.

[[toc]]

## Require Realm Roles

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireRealmRoles_AdminRole_Verified

## Require Resource Roles

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireClientRoles_TestClientRole_Verified

Configure default source:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireClientRoles_TestClientRoleWithConfiguration_Verified

## Keycloak Role Claims Transformation

Keycloak roles can be automatically transformed to [AspNetCore Roles](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles). This feature is disabled by default and is based on `KeycloakRolesClaimsTransformation`.

Specify `KeycloakAuthorizationOptions.EnableRolesMapping` to enable it. E.g.:

```json
{
"Keycloak": {
"EnableRolesMapping": "Realm"
}
}
```

Here an example of how to configure realm role:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireRealmRoles_AdminRoleWithMapping_Verified

Here an example of how to configure client role:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs#RequireClientRoles_TestClientRoleWithMapping_Verified

There are three options to determine a source for the roles:

```csharp
public enum RolesClaimTransformationSource
{
/// <summary>
/// No Transformation. Default
/// </summary>
None,

/// <summary>
/// Use realm roles as source
/// </summary>
Realm,

/// <summary>
/// Use client roles as source
/// </summary>
ResourceAccess
}
```

Here is an example of decoded JWT token:

```json
{
"exp": 1714057504,
"iat": 1714057204,
"jti": "7250d2a9-e5a1-442f-9e76-5e6b78bb2760",
"iss": "http://localhost:8080/realms/Test",
"aud": [
"test-client",
"account"
],
"sub": "bf0b3371-ccdc-44f6-8861-ce25cbfcac39",
"typ": "Bearer",
"azp": "test-client",
"session_state": "563332d2-111a-4ef2-b6a0-ebc1d3ae9a1e",
"acr": "1",
"allowed-origins": [
"/*"
],
"realm_access": {
"roles": [
"default-roles-test",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"test-client": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "profile email",
"sid": "563332d2-111a-4ef2-b6a0-ebc1d3ae9a1e",
"email_verified": false,
"name": "Test Test",
"preferred_username": "test",
"given_name": "Test",
"family_name": "Test",
"email": "[email protected]"
}
```

If we specify `KeycloakAuthorizationOptions.EnableRolesMapping = RolesClaimTransformationSource.Realm` the roles are taken from $token.realm_access.roles.

Result = ["default-roles-test","offline_access","uma_authorization"]

If we specify `KeycloakAuthorizationOptions.EnableRolesMapping = RolesClaimTransformationSource.ResourceAccess` and `KeycloakAuthorizationOptions.RolesResource="test-client"` the roles are taken from $token.realm_access.test-client.roles.

Result = ["manage-account","manage-account-links","view-profile"]

The target claim can be configured `KeycloakAuthorizationOptions.RoleClaimType`, the default value is "role".
2 changes: 1 addition & 1 deletion docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Key Changes in 2.0.0

* `RolesClaimTransformationSource` change to `None` from `ResourceAccess` meaning we no longer map to `AspNetCore` roles by default.
* `RolesClaimTransformationSource` changed to `None` from `ResourceAccess` meaning we no longer map to `AspNetCore` roles by default. Renamed to `EnableRolesMapping`. Moved to `Keycloak.AuthServices.Authorization`.
* Moved `IKeycloakProtectionClient` to `Keycloak.AuthServices.Authorization`. Removed `AddKeycloakProtectionHttpClient`, added `AddAuthorizationServer` instead.

```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"secret": ""
},
"confidential-port": 0,
"RolesSource": "Realm"
"EnableRolesMapping": "Realm"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,10 @@ public class KeycloakAuthenticationOptions : KeycloakInstallationOptions
/// <summary>
/// Gets or sets the claim type used for roles.
/// </summary>
public string RoleClaimType { get; set; } = "role";
public string RoleClaimType { get; set; } = KeycloakConstants.RoleClaimType;

/// <summary>
/// Gets or sets the claim type used for the name.
/// </summary>
public string NameClaimType { get; set; } = "preferred_username";

/// <summary>
/// Determines the source for roles
/// </summary>
public RolesClaimTransformationSource RolesSource { get; set; } =
RolesClaimTransformationSource.None;

/// <summary>
/// The name of the resource to be used. Only relevant for RolesSource = RolesClaimTransformationSource.ResourceAccess
/// </summary>
public string? RolesResource { get; set; }
public string NameClaimType { get; set; } = KeycloakConstants.NameClaimType;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace Keycloak.AuthServices.Authentication;

using Claims;
using Keycloak.AuthServices.Common;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
Expand All @@ -17,30 +16,24 @@ public static class ServiceCollectionExtensions
/// <summary>
/// Adds keycloak authentication services.
/// </summary>
[Obsolete("This method will be removed. Use AddKeycloakWebApiAuthentication. Furthermore, the way KeycloakAuthenticationOptions is changed and you need to specify KeycloakFormatBinder.Instance to correctly bind the instance. See for more details https://nikiforovall.github.io/keycloak-authorization-services-dotnet/migration.html#key-changes-in-2-0-0")]
[Obsolete(
"This method will be removed. Use AddKeycloakWebApiAuthentication. Furthermore, the way KeycloakAuthenticationOptions is changed and you need to specify KeycloakFormatBinder.Instance to correctly bind the instance. See for more details https://nikiforovall.github.io/keycloak-authorization-services-dotnet/migration.html#key-changes-in-2-0-0"
)]
public static AuthenticationBuilder AddKeycloakAuthentication(
this IServiceCollection services,
KeycloakAuthenticationOptions keycloakOptions,
Action<JwtBearerOptions>? configureOptions = default
)
{
const string roleClaimType = "role";
var validationParameters = new TokenValidationParameters
{
ClockSkew = keycloakOptions.TokenClockSkew,
ValidateAudience = keycloakOptions.VerifyTokenAudience ?? true,
ValidateIssuer = true,
NameClaimType = "preferred_username",
RoleClaimType = roleClaimType,
NameClaimType = keycloakOptions.NameClaimType,
RoleClaimType = keycloakOptions.RoleClaimType,
};

// options.Resource == Audience
services.AddTransient<IClaimsTransformation>(_ => new KeycloakRolesClaimsTransformation(
roleClaimType,
keycloakOptions.RolesSource,
keycloakOptions.RolesResource ?? keycloakOptions.Resource
));

return services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
Expand Down
Loading

0 comments on commit 9ba23f4

Please sign in to comment.