0
# Policy Enforcement
1
2
Policy Enforcement Point (PEP) integration for authorization policy evaluation with Keycloak's authorization services. This module provides the foundation for fine-grained authorization policies and resource protection in applications.
3
4
## Capabilities
5
6
### HttpAuthzRequest
7
8
HTTP authorization request wrapper providing access to request information for policy evaluation.
9
10
```java { .api }
11
/**
12
* HTTP authorization request wrapper providing access to request information for policy evaluation
13
*/
14
public class HttpAuthzRequest implements AuthzRequest {
15
/**
16
* Constructor with OIDC HTTP facade
17
* @param oidcFacade OIDC HTTP facade providing request access
18
*/
19
public HttpAuthzRequest(OIDCHttpFacade oidcFacade);
20
21
/**
22
* Get the relative path of the request
23
* @return Relative path string
24
*/
25
public String getRelativePath();
26
27
/**
28
* Get the HTTP method of the request
29
* @return HTTP method (GET, POST, PUT, DELETE, etc.)
30
*/
31
public String getMethod();
32
33
/**
34
* Get the full URI of the request
35
* @return Complete request URI
36
*/
37
public String getURI();
38
39
/**
40
* Get all headers with the specified name
41
* @param name Header name
42
* @return List of header values
43
*/
44
public List<String> getHeaders(String name);
45
46
/**
47
* Get the first parameter value with the specified name
48
* @param name Parameter name
49
* @return First parameter value or null
50
*/
51
public String getFirstParam(String name);
52
53
/**
54
* Get cookie value by name
55
* @param name Cookie name
56
* @return Cookie value or null
57
*/
58
public String getCookieValue(String name);
59
60
/**
61
* Get remote client address
62
* @return Remote IP address
63
*/
64
public String getRemoteAddr();
65
66
/**
67
* Check if the request is secure (HTTPS)
68
* @return true if secure, false otherwise
69
*/
70
public boolean isSecure();
71
72
/**
73
* Get single header value
74
* @param name Header name
75
* @return Header value or null
76
*/
77
public String getHeader(String name);
78
79
/**
80
* Get request input stream
81
* @param buffered Whether to buffer the stream
82
* @return InputStream for request body
83
*/
84
public InputStream getInputStream(boolean buffered);
85
86
/**
87
* Get the authenticated token principal
88
* @return TokenPrincipal containing token information
89
*/
90
public TokenPrincipal getPrincipal();
91
}
92
```
93
94
**Usage Examples:**
95
96
```java
97
// Create authorization request from HTTP facade
98
HttpAuthzRequest authzRequest = new HttpAuthzRequest(oidcFacade);
99
100
// Extract request information for policy evaluation
101
String method = authzRequest.getMethod();
102
String path = authzRequest.getRelativePath();
103
String uri = authzRequest.getURI();
104
String clientIp = authzRequest.getRemoteAddr();
105
boolean isSecure = authzRequest.isSecure();
106
107
// Access headers and parameters
108
String contentType = authzRequest.getHeader("Content-Type");
109
List<String> acceptHeaders = authzRequest.getHeaders("Accept");
110
String actionParam = authzRequest.getFirstParam("action");
111
112
// Access authentication information
113
TokenPrincipal principal = authzRequest.getPrincipal();
114
if (principal != null) {
115
AccessToken token = principal.getToken();
116
String username = token.getPreferredUsername();
117
Set<String> roles = token.getRealmAccess().getRoles();
118
}
119
120
// Read request body if needed
121
try (InputStream inputStream = authzRequest.getInputStream(true)) {
122
// Process request body for policy evaluation
123
String requestBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
124
}
125
```
126
127
### HttpAuthzResponse
128
129
HTTP authorization response wrapper for sending policy enforcement responses.
130
131
```java { .api }
132
/**
133
* HTTP authorization response wrapper for sending policy enforcement responses
134
*/
135
public class HttpAuthzResponse implements AuthzResponse {
136
/**
137
* Constructor with OIDC HTTP facade
138
* @param oidcFacade OIDC HTTP facade providing response access
139
*/
140
public HttpAuthzResponse(OIDCHttpFacade oidcFacade);
141
142
/**
143
* Send error response with status code only
144
* @param statusCode HTTP status code
145
*/
146
public void sendError(int statusCode);
147
148
/**
149
* Send error response with status code and reason
150
* @param code HTTP status code
151
* @param reason Error reason phrase
152
*/
153
public void sendError(int code, String reason);
154
155
/**
156
* Set response header
157
* @param name Header name
158
* @param value Header value
159
*/
160
public void setHeader(String name, String value);
161
}
162
```
163
164
**Usage Examples:**
165
166
```java
167
// Create authorization response
168
HttpAuthzResponse authzResponse = new HttpAuthzResponse(oidcFacade);
169
170
// Send forbidden response for denied requests
171
authzResponse.sendError(403, "Access denied by policy");
172
173
// Send unauthorized response for unauthenticated requests
174
authzResponse.sendError(401, "Authentication required");
175
176
// Add custom headers to response
177
authzResponse.setHeader("X-Policy-Decision", "DENY");
178
authzResponse.setHeader("X-Required-Role", "admin");
179
180
// Send specific error for resource not found
181
authzResponse.sendError(404, "Protected resource not found");
182
```
183
184
## Policy Enforcement Patterns
185
186
### Basic Policy Enforcer Integration
187
188
```java
189
// Policy enforcement filter
190
public class PolicyEnforcementFilter implements Filter {
191
private PolicyEnforcer policyEnforcer;
192
private KeycloakDeployment deployment;
193
194
@Override
195
public void init(FilterConfig filterConfig) throws ServletException {
196
// Initialize deployment and policy enforcer
197
InputStream configStream = getClass().getResourceAsStream("/keycloak.json");
198
deployment = KeycloakDeploymentBuilder.build(configStream);
199
policyEnforcer = deployment.getPolicyEnforcer();
200
}
201
202
@Override
203
public void doFilter(ServletRequest request, ServletResponse response,
204
FilterChain chain) throws IOException, ServletException {
205
HttpServletRequest httpRequest = (HttpServletRequest) request;
206
HttpServletResponse httpResponse = (HttpServletResponse) response;
207
208
// Create OIDC facade
209
OIDCHttpFacade facade = new ServletOIDCHttpFacade(httpRequest, httpResponse);
210
211
// Create authorization request and response
212
HttpAuthzRequest authzRequest = new HttpAuthzRequest(facade);
213
HttpAuthzResponse authzResponse = new HttpAuthzResponse(facade);
214
215
try {
216
// Evaluate policy
217
AuthzResult result = policyEnforcer.enforce(authzRequest, authzResponse);
218
219
if (result.isGranted()) {
220
// Access granted - continue with request
221
chain.doFilter(request, response);
222
} else {
223
// Access denied - response already sent by policy enforcer
224
logger.warn("Access denied for {} {} by user {}",
225
authzRequest.getMethod(),
226
authzRequest.getURI(),
227
authzRequest.getPrincipal() != null ?
228
authzRequest.getPrincipal().getToken().getPreferredUsername() : "anonymous"
229
);
230
}
231
} catch (Exception e) {
232
logger.error("Policy enforcement error", e);
233
authzResponse.sendError(500, "Internal server error");
234
}
235
}
236
}
237
```
238
239
### Custom Policy Evaluation
240
241
```java
242
// Custom policy evaluator
243
public class CustomPolicyEvaluator {
244
private final KeycloakDeployment deployment;
245
246
public CustomPolicyEvaluator(KeycloakDeployment deployment) {
247
this.deployment = deployment;
248
}
249
250
public PolicyDecision evaluate(HttpAuthzRequest request) {
251
TokenPrincipal principal = request.getPrincipal();
252
if (principal == null) {
253
return PolicyDecision.deny("No authentication token");
254
}
255
256
AccessToken token = principal.getToken();
257
String method = request.getMethod();
258
String path = request.getRelativePath();
259
260
// Time-based access control
261
if (isOutsideBusinessHours() && !hasRole(token, "admin")) {
262
return PolicyDecision.deny("Access outside business hours requires admin role");
263
}
264
265
// IP-based access control
266
String clientIp = request.getRemoteAddr();
267
if (!isAllowedIpAddress(clientIp) && isSensitiveResource(path)) {
268
return PolicyDecision.deny("Sensitive resource access not allowed from this IP");
269
}
270
271
// Resource-specific policies
272
if (path.startsWith("/admin")) {
273
if (!hasRole(token, "admin")) {
274
return PolicyDecision.deny("Admin access required");
275
}
276
} else if (path.startsWith("/api/sensitive")) {
277
if (!hasRole(token, "privileged-user") && !isResourceOwner(token, path)) {
278
return PolicyDecision.deny("Insufficient privileges for sensitive API");
279
}
280
}
281
282
// Method-specific policies
283
if ("DELETE".equals(method) && !hasRole(token, "data-manager")) {
284
return PolicyDecision.deny("Delete operations require data-manager role");
285
}
286
287
return PolicyDecision.permit("Access granted");
288
}
289
290
private boolean hasRole(AccessToken token, String role) {
291
return token.getRealmAccess() != null &&
292
token.getRealmAccess().getRoles().contains(role);
293
}
294
295
private boolean isOutsideBusinessHours() {
296
LocalTime now = LocalTime.now();
297
return now.isBefore(LocalTime.of(9, 0)) || now.isAfter(LocalTime.of(17, 0));
298
}
299
300
private boolean isAllowedIpAddress(String ip) {
301
// Check against whitelist of allowed IP ranges
302
return ip.startsWith("10.0.") || ip.startsWith("192.168.") || "127.0.0.1".equals(ip);
303
}
304
305
private boolean isSensitiveResource(String path) {
306
return path.contains("/financial") || path.contains("/personal") || path.contains("/confidential");
307
}
308
309
private boolean isResourceOwner(AccessToken token, String path) {
310
// Extract resource owner from path and compare with token subject
311
String userId = extractUserIdFromPath(path);
312
return token.getSubject().equals(userId);
313
}
314
315
private String extractUserIdFromPath(String path) {
316
// Extract user ID from path like /api/users/{userId}/profile
317
String[] segments = path.split("/");
318
for (int i = 0; i < segments.length - 1; i++) {
319
if ("users".equals(segments[i]) && i + 1 < segments.length) {
320
return segments[i + 1];
321
}
322
}
323
return null;
324
}
325
326
public static class PolicyDecision {
327
private final boolean granted;
328
private final String reason;
329
330
private PolicyDecision(boolean granted, String reason) {
331
this.granted = granted;
332
this.reason = reason;
333
}
334
335
public static PolicyDecision permit(String reason) {
336
return new PolicyDecision(true, reason);
337
}
338
339
public static PolicyDecision deny(String reason) {
340
return new PolicyDecision(false, reason);
341
}
342
343
public boolean isGranted() { return granted; }
344
public String getReason() { return reason; }
345
}
346
}
347
```
348
349
### Resource-Based Authorization
350
351
```java
352
// Resource-based authorization manager
353
public class ResourceAuthorizationManager {
354
private final PolicyEnforcer policyEnforcer;
355
private final Map<String, ResourceDescriptor> resourceMap;
356
357
public ResourceAuthorizationManager(PolicyEnforcer policyEnforcer) {
358
this.policyEnforcer = policyEnforcer;
359
this.resourceMap = buildResourceMap();
360
}
361
362
public boolean isAuthorized(HttpAuthzRequest request, HttpAuthzResponse response) {
363
String path = request.getRelativePath();
364
String method = request.getMethod();
365
366
ResourceDescriptor resource = findResource(path);
367
if (resource != null) {
368
return evaluateResourceAccess(request, response, resource, method);
369
}
370
371
// Default policy for unmapped resources
372
return evaluateDefaultPolicy(request, response);
373
}
374
375
private ResourceDescriptor findResource(String path) {
376
// Find best matching resource pattern
377
return resourceMap.entrySet().stream()
378
.filter(entry -> pathMatches(path, entry.getKey()))
379
.map(Map.Entry::getValue)
380
.findFirst()
381
.orElse(null);
382
}
383
384
private boolean evaluateResourceAccess(HttpAuthzRequest request, HttpAuthzResponse response,
385
ResourceDescriptor resource, String method) {
386
TokenPrincipal principal = request.getPrincipal();
387
if (principal == null) {
388
response.sendError(401, "Authentication required");
389
return false;
390
}
391
392
AccessToken token = principal.getToken();
393
394
// Check required scopes for the method
395
Set<String> requiredScopes = resource.getRequiredScopes(method);
396
if (!hasRequiredScopes(token, requiredScopes)) {
397
response.sendError(403, "Insufficient scopes for " + method + " on " + resource.getName());
398
return false;
399
}
400
401
// Check resource-specific policies
402
if (resource.hasCustomPolicy()) {
403
PolicyDecision decision = resource.evaluateCustomPolicy(request);
404
if (!decision.isGranted()) {
405
response.sendError(403, decision.getReason());
406
return false;
407
}
408
}
409
410
return true;
411
}
412
413
private boolean hasRequiredScopes(AccessToken token, Set<String> requiredScopes) {
414
Set<String> tokenScopes = extractScopes(token);
415
return tokenScopes.containsAll(requiredScopes);
416
}
417
418
private Set<String> extractScopes(AccessToken token) {
419
String scopeClaim = (String) token.getOtherClaims().get("scope");
420
if (scopeClaim != null) {
421
return Set.of(scopeClaim.split(" "));
422
}
423
return Collections.emptySet();
424
}
425
426
private Map<String, ResourceDescriptor> buildResourceMap() {
427
Map<String, ResourceDescriptor> map = new HashMap<>();
428
429
// Define resources and their access requirements
430
map.put("/api/users/**", ResourceDescriptor.builder()
431
.name("User Management")
432
.scope("GET", "read:users")
433
.scope("POST", "create:users")
434
.scope("PUT", "update:users")
435
.scope("DELETE", "delete:users")
436
.customPolicy((request) -> {
437
// Custom policy: users can only access their own data
438
String path = request.getRelativePath();
439
String tokenSubject = request.getPrincipal().getToken().getSubject();
440
return path.contains("/" + tokenSubject + "/") ||
441
hasRole(request.getPrincipal().getToken(), "admin");
442
})
443
.build());
444
445
map.put("/api/documents/**", ResourceDescriptor.builder()
446
.name("Document Management")
447
.scope("GET", "read:documents")
448
.scope("POST", "create:documents")
449
.scope("PUT", "update:documents")
450
.scope("DELETE", "delete:documents")
451
.customPolicy((request) -> {
452
// Check document ownership or shared access
453
return checkDocumentAccess(request);
454
})
455
.build());
456
457
return map;
458
}
459
460
private boolean pathMatches(String path, String pattern) {
461
// Simple pattern matching (could use more sophisticated matching)
462
if (pattern.endsWith("/**")) {
463
String prefix = pattern.substring(0, pattern.length() - 3);
464
return path.startsWith(prefix);
465
}
466
return path.equals(pattern);
467
}
468
469
// Resource descriptor class
470
public static class ResourceDescriptor {
471
private final String name;
472
private final Map<String, Set<String>> methodScopes;
473
private final Function<HttpAuthzRequest, Boolean> customPolicy;
474
475
// Builder pattern implementation...
476
477
public Set<String> getRequiredScopes(String method) {
478
return methodScopes.getOrDefault(method.toUpperCase(), Collections.emptySet());
479
}
480
481
public boolean hasCustomPolicy() {
482
return customPolicy != null;
483
}
484
485
public PolicyDecision evaluateCustomPolicy(HttpAuthzRequest request) {
486
if (customPolicy != null) {
487
boolean allowed = customPolicy.apply(request);
488
return allowed ?
489
PolicyDecision.permit("Custom policy allows access") :
490
PolicyDecision.deny("Custom policy denies access");
491
}
492
return PolicyDecision.permit("No custom policy");
493
}
494
}
495
}
496
```
497
498
### Claim-Based Authorization
499
500
```java
501
// Claim-based authorization processor
502
public class ClaimBasedAuthorizationProcessor {
503
504
public AuthorizationDecision evaluate(HttpAuthzRequest request) {
505
TokenPrincipal principal = request.getPrincipal();
506
if (principal == null) {
507
return AuthorizationDecision.deny("No authentication token");
508
}
509
510
AccessToken token = principal.getToken();
511
Map<String, Object> claims = token.getOtherClaims();
512
513
// Department-based access control
514
String department = (String) claims.get("department");
515
String resource = request.getRelativePath();
516
517
if (resource.startsWith("/hr/") && !"HR".equals(department)) {
518
return AuthorizationDecision.deny("HR resources require HR department");
519
}
520
521
if (resource.startsWith("/finance/") && !"Finance".equals(department)) {
522
return AuthorizationDecision.deny("Finance resources require Finance department");
523
}
524
525
// Location-based access control
526
String userLocation = (String) claims.get("office_location");
527
String clientIp = request.getRemoteAddr();
528
529
if (!isLocationAllowed(userLocation, clientIp, resource)) {
530
return AuthorizationDecision.deny("Resource not accessible from current location");
531
}
532
533
// Time-based access control with user-specific rules
534
Integer maxHours = (Integer) claims.get("max_access_hours");
535
if (maxHours != null && isOutsideAllowedHours(maxHours)) {
536
return AuthorizationDecision.deny("Access outside allowed hours for user");
537
}
538
539
// Data classification access control
540
String clearanceLevel = (String) claims.get("clearance_level");
541
String resourceClassification = extractResourceClassification(resource);
542
543
if (!hasSufficientClearance(clearanceLevel, resourceClassification)) {
544
return AuthorizationDecision.deny("Insufficient clearance for resource classification");
545
}
546
547
return AuthorizationDecision.permit("All claim-based policies satisfied");
548
}
549
550
private boolean isLocationAllowed(String userLocation, String clientIp, String resource) {
551
// Implement location-based access logic
552
if ("REMOTE".equals(userLocation)) {
553
// Remote users have limited access
554
return !resource.contains("/sensitive/");
555
}
556
return true; // Office users have full access
557
}
558
559
private boolean isOutsideAllowedHours(int maxHours) {
560
int currentHour = LocalTime.now().getHour();
561
return currentHour >= maxHours;
562
}
563
564
private String extractResourceClassification(String resource) {
565
if (resource.contains("/classified/")) return "SECRET";
566
if (resource.contains("/confidential/")) return "CONFIDENTIAL";
567
if (resource.contains("/internal/")) return "INTERNAL";
568
return "PUBLIC";
569
}
570
571
private boolean hasSufficientClearance(String userClearance, String resourceClassification) {
572
Map<String, Integer> clearanceLevels = Map.of(
573
"PUBLIC", 0,
574
"INTERNAL", 1,
575
"CONFIDENTIAL", 2,
576
"SECRET", 3
577
);
578
579
int userLevel = clearanceLevels.getOrDefault(userClearance, 0);
580
int requiredLevel = clearanceLevels.getOrDefault(resourceClassification, 0);
581
582
return userLevel >= requiredLevel;
583
}
584
585
public static class AuthorizationDecision {
586
private final boolean granted;
587
private final String reason;
588
589
private AuthorizationDecision(boolean granted, String reason) {
590
this.granted = granted;
591
this.reason = reason;
592
}
593
594
public static AuthorizationDecision permit(String reason) {
595
return new AuthorizationDecision(true, reason);
596
}
597
598
public static AuthorizationDecision deny(String reason) {
599
return new AuthorizationDecision(false, reason);
600
}
601
602
public boolean isGranted() { return granted; }
603
public String getReason() { return reason; }
604
}
605
}
606
```