-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathKeycloakRolesClaimsTransformation.cs
138 lines (118 loc) · 4.8 KB
/
KeycloakRolesClaimsTransformation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
namespace Keycloak.AuthServices.Authentication.Claims;
using System.Security.Claims;
using System.Text.Json;
using Keycloak.AuthServices.Common;
using Microsoft.AspNetCore.Authentication;
/// <summary>
/// Transforms keycloak roles in the resource_access claim to jwt role claims.
/// Note, realm roles are not mapped atm.
/// </summary>
/// <example>
/// Example of keycloack resource_access claim
/// "resource_access": {
/// "api": {
/// "roles": [ "role1", "role2" ]
/// },
/// "account": {
/// "roles": [
/// "view-profile"
/// ]
/// }
/// },
/// </example>
/// <seealso cref="IClaimsTransformation" />
public class KeycloakRolesClaimsTransformation : IClaimsTransformation
{
private readonly string roleClaimType;
private readonly RolesClaimTransformationSource roleSource;
private readonly string audience;
/// <summary>
/// Initializes a new instance of the <see cref="KeycloakRolesClaimsTransformation"/> class.
/// </summary>
/// <param name="roleClaimType">Type of the role claim.</param>
/// <param name="roleSource"><see cref="RolesClaimTransformationSource"/></param>
/// <param name="audience">The audience.</param>
public KeycloakRolesClaimsTransformation(
string roleClaimType,
RolesClaimTransformationSource roleSource,
string audience)
{
this.roleClaimType = roleClaimType;
this.roleSource = roleSource;
this.audience = audience;
}
/// <summary>
/// Provides a central transformation point to change the specified principal.
/// Note: this will be run on each AuthenticateAsync call, so its safer to
/// return a new ClaimsPrincipal if your transformation is not idempotent.
/// </summary>
/// <param name="principal">The <see cref="ClaimsPrincipal" /> to transform.</param>
/// <returns>
/// The transformed principal.
/// </returns>
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var result = principal.Clone();
if (result.Identity is not ClaimsIdentity identity)
{
return Task.FromResult(result);
}
if (this.roleSource == RolesClaimTransformationSource.ResourceAccess)
{
var resourceAccessValue = principal.FindFirst("resource_access")?.Value;
if (string.IsNullOrWhiteSpace(resourceAccessValue))
{
return Task.FromResult(result);
}
using var resourceAccess = JsonDocument.Parse(resourceAccessValue);
var containsAudienceRoles = resourceAccess
.RootElement
.TryGetProperty(this.audience, out var rolesElement);
if (!containsAudienceRoles)
{
return Task.FromResult(result);
}
var clientRoles = rolesElement.GetProperty("roles");
foreach (var role in clientRoles.EnumerateArray())
{
var value = role.GetString();
var matchingClaim = identity.Claims.FirstOrDefault(claim =>
claim.Type.Equals(this.roleClaimType, StringComparison.InvariantCultureIgnoreCase) &&
claim.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (matchingClaim is null && !string.IsNullOrWhiteSpace(value))
{
identity.AddClaim(new Claim(this.roleClaimType, value));
}
}
return Task.FromResult(result);
}
if (this.roleSource == RolesClaimTransformationSource.Realm)
{
var realmAccessValue = principal.FindFirst("realm_access")?.Value;
if (string.IsNullOrWhiteSpace(realmAccessValue))
{
return Task.FromResult(result);
}
using var realmAccess = JsonDocument.Parse(realmAccessValue);
var containsRoles = realmAccess
.RootElement
.TryGetProperty("roles", out var rolesElement);
if (containsRoles)
{
foreach (var role in rolesElement.EnumerateArray())
{
var value = role.GetString();
var matchingClaim = identity.Claims.FirstOrDefault(claim =>
claim.Type.Equals(this.roleClaimType, StringComparison.InvariantCultureIgnoreCase) &&
claim.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (matchingClaim is null && !string.IsNullOrWhiteSpace(value))
{
identity.AddClaim(new Claim(this.roleClaimType, value));
}
}
return Task.FromResult(result);
}
}
return Task.FromResult(result);
}
}