-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathKcBearerAuthorizationHandler.cs
More file actions
240 lines (203 loc) · 10 KB
/
KcBearerAuthorizationHandler.cs
File metadata and controls
240 lines (203 loc) · 10 KB
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NETCore.Keycloak.Client.Authorization.Requirements;
using NETCore.Keycloak.Client.Exceptions;
using NETCore.Keycloak.Client.HttpClients.Implementation;
using NETCore.Keycloak.Client.Utils;
namespace NETCore.Keycloak.Client.Authorization.Handlers;
/// <summary>
/// Authorization handler for Keycloak that validates user sessions and permissions for accessing protected resources.
/// </summary>
public class KcBearerAuthorizationHandler : AuthorizationHandler<KcAuthorizationRequirement>
{
/// <summary>
/// Logger for logging Keycloak authorization events.
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// HTTP context accessor used to extract authorization tokens from the current HTTP request.
/// </summary>
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Realm admin token handler responsible for retrieving admin tokens for validating user sessions.
/// </summary>
private readonly IKcRealmAdminTokenHandler _realmAdminTokenHandler;
/// <summary>
/// Initializes a new instance of the <see cref="KcBearerAuthorizationHandler"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider used to create scoped services.</param>
public KcBearerAuthorizationHandler(IServiceProvider serviceProvider)
{
// Create a service scope for resolving scoped dependencies.
using var scope = serviceProvider.CreateScope();
// Resolve the logger instance.
_logger = scope.ServiceProvider.GetService<ILogger<KcBearerAuthorizationHandler>>();
// Resolve the HTTP context accessor for accessing request data.
_httpContextAccessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
// Resolve the realm admin token handler for retrieving admin tokens.
_realmAdminTokenHandler = scope.ServiceProvider.GetRequiredService<IKcRealmAdminTokenHandler>();
}
/// <summary>
/// Handles the authorization requirement by validating the user session and checking permissions for protected resources.
/// </summary>
/// <param name="context">The authorization handler context.</param>
/// <param name="requirement">The authorization requirement defining the protected resource and permissions.</param>
/// <exception cref="ArgumentNullException">Thrown if the context or requirement is null.</exception>
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
KcAuthorizationRequirement requirement)
{
// Ensure that authorization context is not null
ArgumentNullException.ThrowIfNull(context);
// Ensure that authorization requirement is not null.
ArgumentNullException.ThrowIfNull(requirement);
// Check if the user is authenticated
if ( context.User.Identity?.IsAuthenticated ?? false )
{
// Extract the authorization header value
var authorizationData =
_httpContextAccessor?.HttpContext?.Request.Headers.Authorization.ToString().Split(" ");
// Ensure the authorization header uses the Bearer scheme and contains a token
if ( authorizationData == null || authorizationData.Length < 2 ||
authorizationData[0] != JwtBearerDefaults.AuthenticationScheme )
{
return; // No action if not Bearer scheme
}
// Extract and validate the Bearer token
var identityToken = authorizationData.ElementAtOrDefault(1);
if ( string.IsNullOrWhiteSpace(identityToken) )
{
context.Fail();
return;
}
// Extract the base URL and realm name from the token's issuer claim
var (baseUrl, realm) = TryExtractRealm(identityToken);
// Fail the context if the base URL or realm name is missing
if ( string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(realm) )
{
context.Fail();
return;
}
// Fetch protected resources and check for authorization
var protectedResource = requirement.ProtectedResourceStore.GetRealmProtectedResources()
.FirstOrDefault(resource => resource.Realm == realm)?.ProtectedResourceName;
if ( protectedResource != null )
{
// Initialize the Keycloak client
var keycloakClient = new KeycloakClient(baseUrl, _logger);
// Validate the user session
await ValidateUserSession(context, keycloakClient, realm).ConfigureAwait(false);
// Request party token and check for access to the protected resource
var rptResponse = await keycloakClient.Auth.GetRequestPartyTokenAsync(realm, identityToken,
protectedResource, [requirement.ToString()])
.ConfigureAwait(false);
if ( rptResponse.IsError )
{
// Log an error if the request party token (RPT) request failed
KcLoggerMessages.Error(_logger,
$"Access to {protectedResource} resource {requirement} permission is denied",
rptResponse.Exception);
context.Fail();
return;
}
// Succeed the authorization context if access is granted
context.Succeed(requirement);
return;
}
// Fail the context if the protected resource is not found
context.Fail();
}
}
/// <summary>
/// Extracts the base URL and realm name from the token's issuer claim.
/// </summary>
/// <param name="accessToken">The JWT access token.</param>
/// <returns>A tuple containing the base URL and realm name.</returns>
private (string, string) TryExtractRealm(string accessToken)
{
if ( string.IsNullOrWhiteSpace(accessToken) )
{
return (null, null);
}
try
{
// Read the JWT token
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(accessToken);
// Extract the issuer claim
var issuer = token.Claims.FirstOrDefault(claim => claim.Type == "iss")?.Value;
if ( string.IsNullOrWhiteSpace(issuer) )
{
return (null, null);
}
// Parse the issuer URL to extract the base URL and realm name
var urlData = new Uri(issuer);
return ($"{urlData.Scheme}://{urlData.Authority}",
urlData.AbsolutePath.Replace("/realms/", string.Empty, StringComparison.Ordinal));
}
catch ( Exception e )
{
// Log an error if unable to extract the issuer
KcLoggerMessages.Error(_logger, "Unable to extract issuer from token", e);
return (null, null);
}
}
/// <summary>
/// Validates the user's session to ensure the session is active and valid.
/// </summary>
/// <param name="context">The authorization handler context.</param>
/// <param name="keycloakClient">The Keycloak client for interacting with Keycloak APIs.</param>
/// <param name="realm">The realm name for validating the session.</param>
/// <exception cref="ArgumentNullException">Thrown if the realm is null or empty.</exception>
/// <exception cref="KcUserNotFoundException">Thrown if the user cannot be found.</exception>
/// <exception cref="KcSessionClosedException">Thrown if the user session is not active.</exception>
private async Task ValidateUserSession(AuthorizationHandlerContext context, KeycloakClient keycloakClient,
string realm)
{
// Ensure the realm name is provided
if ( string.IsNullOrWhiteSpace(realm) )
{
throw new ArgumentNullException(nameof(realm), "Realm is required.");
}
// Retrieve the admin token for the specified realm
var adminToken = await _realmAdminTokenHandler.TryGetAdminTokenAsync(realm).ConfigureAwait(false);
// Extract the user ID from the claims
var userId = context.User.Claims
.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value;
if ( string.IsNullOrWhiteSpace(userId) )
{
throw new KcUserNotFoundException("Unable to extract user subject.");
}
// Check if the user exists in Keycloak
var userResponse = await keycloakClient.Users.GetAsync(realm, adminToken, userId).ConfigureAwait(false);
if ( userResponse.IsError )
{
throw new KcUserNotFoundException($"User {userId} not found. Error: {userResponse.ErrorMessage}",
userResponse.Exception);
}
// Extract the session ID from the claims
var sessionId = context.User.Claims.FirstOrDefault(claim => claim.Type == "sid")?.Value;
if ( string.IsNullOrWhiteSpace(sessionId) )
{
throw new KcSessionClosedException("Unable to extract session ID.");
}
// Retrieve active sessions for the user
var sessionsResponse =
await keycloakClient.Users.SessionsAsync(realm, adminToken, userId).ConfigureAwait(false);
if ( sessionsResponse.IsError )
{
throw new KcSessionClosedException($"No active session found for user {userId}.",
userResponse.Exception);
}
// Ensure the session ID exists among active sessions
if ( sessionsResponse.Response.All(session => session.Id != sessionId) )
{
throw new KcSessionClosedException($"Session {sessionId} not found for user {userId}.",
userResponse.Exception);
}
}
}