Skip to content

Commit

Permalink
feat: Add Authorizatoin Server vNext (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll authored May 1, 2024
1 parent 9ba23f4 commit 591063f
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 10 deletions.
10 changes: 8 additions & 2 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ export default defineConfig({
{ text: 'Authorization', link: '/configuration/configuration-authorization' },
{ text: 'Keycloak', link: '/configuration/configuration-keycloak' },
]
}
,
},
{
text: 'Authorization',
collapsed: false,
items: [
{ text: 'Authorization Server', link: '/authorization/authorization-server' },
]
},
{
text: 'Examples',
collapsed: false,
Expand Down
20 changes: 20 additions & 0 deletions docs/authorization/authorization-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Authorization Server

Keycloak is an open-source Identity and Access Management solution that provides an Authorization Server. The Authorization Server is responsible for access to clients after successfully authenticating and authorizing the users.

In addition to the Authorization Server, Keycloak also supports a *Policy Enforcement Point* (PEP). The PEP is responsible for enforcing access control policies and protecting resources. It intercepts requests from clients and verifies the access token before allowing or denying access to the requested resource.

By integrating Keycloak's Authorization Server and PEP into your application, you can implement fine-grained access control and secure your resources based on user roles, permissions, and other attributes.

## Evaluate Permissions

Assume we have a default resource with Name *"urn:test-client:resources:default"*.

We want to check if a given user has access to it. It is accomplished based on permissions. In our case default permission is applied to default resource type. *"Default Permission"* is based on policy named - *"Require Admin Role"*. This policy checks if a user has *"Admin"* realm role.

Here is how to do it from code:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AuthorizationServerPolicyTests.cs#RequireProtectedResource_DefaultResource_Verified

> [!Note]
> The calls to Authorization Servers are made on behalf of a user based on header propagation. We are taking user's *access_token* (JWT Bearer Token) from `IHttpContextAccessor`. `AddHeaderPropagation` adds `AccessTokenPropagationHandler` delegating handler to `IKeycloakProtectionClient` responsible for header propagation.
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ CancellationToken cancellationToken
{
var audience = this.clientOptions.Value.Resource;

var permission = string.IsNullOrWhiteSpace(scope) ? resource : $"{resource}#{scope}";

var data = new Dictionary<string, string>
{
{ "grant_type", "urn:ietf:params:oauth:grant-type:uma-ticket" },
{ "response_mode", "decision" },
{ "audience", audience ?? string.Empty },
{ "permission", $"{resource}#{scope}" }
{ "permission", permission }
};

var response = await this.httpClient.PostAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public string KeycloakUrlRealm
return default!;
}

return $"{NormalizeUrl(this.AuthServerUrl)}/realms/{this.Realm}";
return $"{NormalizeUrl(this.AuthServerUrl)}/realms/{this.Realm}/";
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace Keycloak.AuthServices.IntegrationTests;

using System.Net;
using Alba;
using Alba.Security;
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
using static Keycloak.AuthServices.IntegrationTests.Utils;

public class AuthorizationServerPolicyTests(
KeycloakFixture fixture,
ITestOutputHelper testOutputHelper
) : AuthenticationScenario(fixture)
{
private static readonly string AppSettings = "appsettings.json";

[Fact]
public async Task RequireProtectedResource_DefaultResource_Verified()
{
var policyName = "RequireProtectedResource";
await using var host = await AlbaHost.For<Program>(
x =>
{
x.WithLogging(testOutputHelper);
x.UseConfiguration(AppSettings);
x.ConfigureServices(
(context, services) =>
{
#region RequireProtectedResource_DefaultResource_Verified
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddKeycloakWebApi(context.Configuration);
services
.AddAuthorization()
.AddKeycloakAuthorization()
.AddAuthorizationBuilder()
.AddPolicy(
policyName,
policy =>
policy.RequireProtectedResource(
resource: "urn:test-client:resources:default",
scope: string.Empty
)
);
services
.AddAuthorizationServer(context.Configuration)
.AddStandardResilienceHandler(); // an example of how to extend IKeycloakProtectionClient by adding Polly
#endregion RequireProtectedResource_DefaultResource_Verified
services.PostConfigure<JwtBearerOptions>(options =>
options.WithKeycloakFixture(this.Keycloak)
);
}
);
},
UserPasswordFlow(ReadKeycloakAuthenticationOptions(AppSettings))
);

await host.Scenario(_ =>
{
_.Get.Url(RunPolicyBuyName(policyName));
_.UserAndPasswordIs(TestUsersRegistry.Admin.UserName, TestUsersRegistry.Admin.Password);
_.StatusCodeShouldBe(HttpStatusCode.OK);
});

await host.Scenario(_ =>
{
_.Get.Url(RunPolicyBuyName(policyName));
_.UserAndPasswordIs(
TestUsersRegistry.Tester.UserName,
TestUsersRegistry.Tester.Password
);
_.StatusCodeShouldBe(HttpStatusCode.Forbidden);
});
}

private static string RunPolicyBuyName(string policyName) =>
$"/endpoints/RunPolicyBuyName?policy={policyName}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Alba" Version="7.4.1" />
<PackageReference Include="Meziantou.Extensions.Logging.Xunit" Version="1.0.7" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.4.0" />
<PackageReference Include="Testcontainers" Version="3.8.0" />
<PackageReference Include="Testcontainers.Keycloak" Version="3.8.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@
"security-admin-console" : [ ],
"admin-cli" : [ ],
"test-client" : [ {
"id" : "9bf9ae1c-95c2-4aee-b04b-6d3ffc0cfe6a",
"name" : "uma_protection",
"composite" : false,
"clientRole" : true,
"containerId" : "0751463c-5267-483b-84b5-2ea45013838c",
"attributes" : { }
}, {
"id" : "2eedf79d-cc99-4ef4-885a-0dd67ace5c22",
"name" : "TestClientRole",
"description" : "",
Expand Down Expand Up @@ -631,7 +638,8 @@
"standardFlowEnabled" : true,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : true,
"serviceAccountsEnabled" : false,
"serviceAccountsEnabled" : true,
"authorizationServicesEnabled" : true,
"publicClient" : false,
"frontchannelLogout" : true,
"protocol" : "openid-connect",
Expand All @@ -641,6 +649,7 @@
"backchannel.logout.session.required" : "true",
"post.logout.redirect.uris" : "+",
"oauth2.device.authorization.grant.enabled" : "false",
"display.on.consent.screen" : "false",
"backchannel.logout.revoke.offline.tokens" : "false"
},
"authenticationFlowBindingOverrides" : { },
Expand Down Expand Up @@ -690,7 +699,45 @@
}
} ],
"defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "email" ],
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
"authorizationSettings" : {
"allowRemoteResourceManagement" : true,
"policyEnforcementMode" : "ENFORCING",
"resources" : [ {
"name" : "urn:test-client:resources:default",
"type" : "urn:test-client:resources:default",
"ownerManagedAccess" : false,
"displayName" : "",
"attributes" : { },
"_id" : "605c8302-4d45-4d60-aff1-9d1727ddde5f",
"uris" : [ "/*" ],
"icon_uri" : ""
} ],
"policies" : [ {
"id" : "04034aae-7d80-4ad4-902f-c8e57211b4c7",
"name" : "Require Admin Role",
"description" : "",
"type" : "role",
"logic" : "POSITIVE",
"decisionStrategy" : "UNANIMOUS",
"config" : {
"roles" : "[{\"id\":\"Admin\",\"required\":true}]"
}
}, {
"id" : "90c2fe20-481c-4d32-b5e7-04cca9f8d9a5",
"name" : "Default Permission",
"description" : "A permission that applies to the default resource type",
"type" : "resource",
"logic" : "POSITIVE",
"decisionStrategy" : "UNANIMOUS",
"config" : {
"defaultResourceType" : "urn:test-client:resources:default",
"applyPolicies" : "[\"Require Admin Role\"]"
}
} ],
"scopes" : [ ],
"decisionStrategy" : "UNANIMOUS"
}
} ],
"clientScopes" : [ {
"id" : "04a2958c-7f06-417c-8609-ad861436645a",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
{
"realm" : "Test",
"users" : [ {
"id" : "7bd66f78-377e-4685-b3ab-eea4f5952645",
"username" : "service-account-test-client",
"emailVerified" : false,
"createdTimestamp" : 1714575417572,
"enabled" : true,
"totp" : false,
"serviceAccountClientId" : "test-client",
"credentials" : [ ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "default-roles-test" ],
"clientRoles" : {
"test-client" : [ "uma_protection" ]
},
"notBefore" : 0,
"groups" : [ ]
}, {
"id" : "53a698f2-972b-4dcc-8db5-62a388bb8022",
"username" : "test",
"firstName" : "TestFirstName",
Expand Down
6 changes: 3 additions & 3 deletions tests/Keycloak.AuthServices.IntegrationTests/PolicyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ await host.Scenario(_ =>
[Fact]
public async Task RequireClientRoles_TestClientRole_Verified()
{
var policyName = "RequireResourceRoles";
var policyName = "RequireResourceRolesWithSource";
await using var host = await AlbaHost.For<Program>(
x =>
{
Expand Down Expand Up @@ -193,7 +193,7 @@ await host.Scenario(_ =>
[Fact]
public async Task RequireRealmRoles_AdminRoleWithMapping_Verified()
{
var policyName = "RequireRealmRole";
var policyName = "RequireRole";
await using var host = await AlbaHost.For<Program>(
x =>
{
Expand Down Expand Up @@ -254,7 +254,7 @@ await host.Scenario(_ =>
[Fact]
public async Task RequireClientRoles_TestClientRoleWithMapping_Verified()
{
var policyName = "RequireResourceRoles";
var policyName = "RequireRole";
await using var host = await AlbaHost.For<Program>(
x =>
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Keycloak.AuthServices.IntegrationTests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Inside docker container run:

```bash
/opt/keycloak/bin/kc.sh export --dir /opt/keycloak/data/import --real Test
/opt/keycloak/bin/kc.sh export --dir /opt/keycloak/data/import --realm Test
```

## User Registry in Test Realm
Expand Down

0 comments on commit 591063f

Please sign in to comment.