Core functionality for Keycloak OIDC/OAuth2 client adapters enabling Java applications to integrate with Keycloak identity and access management services
—
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.
HTTP authorization request wrapper providing access to request information for policy evaluation.
/**
* HTTP authorization request wrapper providing access to request information for policy evaluation
*/
public class HttpAuthzRequest implements AuthzRequest {
/**
* Constructor with OIDC HTTP facade
* @param oidcFacade OIDC HTTP facade providing request access
*/
public HttpAuthzRequest(OIDCHttpFacade oidcFacade);
/**
* Get the relative path of the request
* @return Relative path string
*/
public String getRelativePath();
/**
* Get the HTTP method of the request
* @return HTTP method (GET, POST, PUT, DELETE, etc.)
*/
public String getMethod();
/**
* Get the full URI of the request
* @return Complete request URI
*/
public String getURI();
/**
* Get all headers with the specified name
* @param name Header name
* @return List of header values
*/
public List<String> getHeaders(String name);
/**
* Get the first parameter value with the specified name
* @param name Parameter name
* @return First parameter value or null
*/
public String getFirstParam(String name);
/**
* Get cookie value by name
* @param name Cookie name
* @return Cookie value or null
*/
public String getCookieValue(String name);
/**
* Get remote client address
* @return Remote IP address
*/
public String getRemoteAddr();
/**
* Check if the request is secure (HTTPS)
* @return true if secure, false otherwise
*/
public boolean isSecure();
/**
* Get single header value
* @param name Header name
* @return Header value or null
*/
public String getHeader(String name);
/**
* Get request input stream
* @param buffered Whether to buffer the stream
* @return InputStream for request body
*/
public InputStream getInputStream(boolean buffered);
/**
* Get the authenticated token principal
* @return TokenPrincipal containing token information
*/
public TokenPrincipal getPrincipal();
}Usage Examples:
// Create authorization request from HTTP facade
HttpAuthzRequest authzRequest = new HttpAuthzRequest(oidcFacade);
// Extract request information for policy evaluation
String method = authzRequest.getMethod();
String path = authzRequest.getRelativePath();
String uri = authzRequest.getURI();
String clientIp = authzRequest.getRemoteAddr();
boolean isSecure = authzRequest.isSecure();
// Access headers and parameters
String contentType = authzRequest.getHeader("Content-Type");
List<String> acceptHeaders = authzRequest.getHeaders("Accept");
String actionParam = authzRequest.getFirstParam("action");
// Access authentication information
TokenPrincipal principal = authzRequest.getPrincipal();
if (principal != null) {
AccessToken token = principal.getToken();
String username = token.getPreferredUsername();
Set<String> roles = token.getRealmAccess().getRoles();
}
// Read request body if needed
try (InputStream inputStream = authzRequest.getInputStream(true)) {
// Process request body for policy evaluation
String requestBody = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
}HTTP authorization response wrapper for sending policy enforcement responses.
/**
* HTTP authorization response wrapper for sending policy enforcement responses
*/
public class HttpAuthzResponse implements AuthzResponse {
/**
* Constructor with OIDC HTTP facade
* @param oidcFacade OIDC HTTP facade providing response access
*/
public HttpAuthzResponse(OIDCHttpFacade oidcFacade);
/**
* Send error response with status code only
* @param statusCode HTTP status code
*/
public void sendError(int statusCode);
/**
* Send error response with status code and reason
* @param code HTTP status code
* @param reason Error reason phrase
*/
public void sendError(int code, String reason);
/**
* Set response header
* @param name Header name
* @param value Header value
*/
public void setHeader(String name, String value);
}Usage Examples:
// Create authorization response
HttpAuthzResponse authzResponse = new HttpAuthzResponse(oidcFacade);
// Send forbidden response for denied requests
authzResponse.sendError(403, "Access denied by policy");
// Send unauthorized response for unauthenticated requests
authzResponse.sendError(401, "Authentication required");
// Add custom headers to response
authzResponse.setHeader("X-Policy-Decision", "DENY");
authzResponse.setHeader("X-Required-Role", "admin");
// Send specific error for resource not found
authzResponse.sendError(404, "Protected resource not found");// Policy enforcement filter
public class PolicyEnforcementFilter implements Filter {
private PolicyEnforcer policyEnforcer;
private KeycloakDeployment deployment;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialize deployment and policy enforcer
InputStream configStream = getClass().getResourceAsStream("/keycloak.json");
deployment = KeycloakDeploymentBuilder.build(configStream);
policyEnforcer = deployment.getPolicyEnforcer();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Create OIDC facade
OIDCHttpFacade facade = new ServletOIDCHttpFacade(httpRequest, httpResponse);
// Create authorization request and response
HttpAuthzRequest authzRequest = new HttpAuthzRequest(facade);
HttpAuthzResponse authzResponse = new HttpAuthzResponse(facade);
try {
// Evaluate policy
AuthzResult result = policyEnforcer.enforce(authzRequest, authzResponse);
if (result.isGranted()) {
// Access granted - continue with request
chain.doFilter(request, response);
} else {
// Access denied - response already sent by policy enforcer
logger.warn("Access denied for {} {} by user {}",
authzRequest.getMethod(),
authzRequest.getURI(),
authzRequest.getPrincipal() != null ?
authzRequest.getPrincipal().getToken().getPreferredUsername() : "anonymous"
);
}
} catch (Exception e) {
logger.error("Policy enforcement error", e);
authzResponse.sendError(500, "Internal server error");
}
}
}// Custom policy evaluator
public class CustomPolicyEvaluator {
private final KeycloakDeployment deployment;
public CustomPolicyEvaluator(KeycloakDeployment deployment) {
this.deployment = deployment;
}
public PolicyDecision evaluate(HttpAuthzRequest request) {
TokenPrincipal principal = request.getPrincipal();
if (principal == null) {
return PolicyDecision.deny("No authentication token");
}
AccessToken token = principal.getToken();
String method = request.getMethod();
String path = request.getRelativePath();
// Time-based access control
if (isOutsideBusinessHours() && !hasRole(token, "admin")) {
return PolicyDecision.deny("Access outside business hours requires admin role");
}
// IP-based access control
String clientIp = request.getRemoteAddr();
if (!isAllowedIpAddress(clientIp) && isSensitiveResource(path)) {
return PolicyDecision.deny("Sensitive resource access not allowed from this IP");
}
// Resource-specific policies
if (path.startsWith("/admin")) {
if (!hasRole(token, "admin")) {
return PolicyDecision.deny("Admin access required");
}
} else if (path.startsWith("/api/sensitive")) {
if (!hasRole(token, "privileged-user") && !isResourceOwner(token, path)) {
return PolicyDecision.deny("Insufficient privileges for sensitive API");
}
}
// Method-specific policies
if ("DELETE".equals(method) && !hasRole(token, "data-manager")) {
return PolicyDecision.deny("Delete operations require data-manager role");
}
return PolicyDecision.permit("Access granted");
}
private boolean hasRole(AccessToken token, String role) {
return token.getRealmAccess() != null &&
token.getRealmAccess().getRoles().contains(role);
}
private boolean isOutsideBusinessHours() {
LocalTime now = LocalTime.now();
return now.isBefore(LocalTime.of(9, 0)) || now.isAfter(LocalTime.of(17, 0));
}
private boolean isAllowedIpAddress(String ip) {
// Check against whitelist of allowed IP ranges
return ip.startsWith("10.0.") || ip.startsWith("192.168.") || "127.0.0.1".equals(ip);
}
private boolean isSensitiveResource(String path) {
return path.contains("/financial") || path.contains("/personal") || path.contains("/confidential");
}
private boolean isResourceOwner(AccessToken token, String path) {
// Extract resource owner from path and compare with token subject
String userId = extractUserIdFromPath(path);
return token.getSubject().equals(userId);
}
private String extractUserIdFromPath(String path) {
// Extract user ID from path like /api/users/{userId}/profile
String[] segments = path.split("/");
for (int i = 0; i < segments.length - 1; i++) {
if ("users".equals(segments[i]) && i + 1 < segments.length) {
return segments[i + 1];
}
}
return null;
}
public static class PolicyDecision {
private final boolean granted;
private final String reason;
private PolicyDecision(boolean granted, String reason) {
this.granted = granted;
this.reason = reason;
}
public static PolicyDecision permit(String reason) {
return new PolicyDecision(true, reason);
}
public static PolicyDecision deny(String reason) {
return new PolicyDecision(false, reason);
}
public boolean isGranted() { return granted; }
public String getReason() { return reason; }
}
}// Resource-based authorization manager
public class ResourceAuthorizationManager {
private final PolicyEnforcer policyEnforcer;
private final Map<String, ResourceDescriptor> resourceMap;
public ResourceAuthorizationManager(PolicyEnforcer policyEnforcer) {
this.policyEnforcer = policyEnforcer;
this.resourceMap = buildResourceMap();
}
public boolean isAuthorized(HttpAuthzRequest request, HttpAuthzResponse response) {
String path = request.getRelativePath();
String method = request.getMethod();
ResourceDescriptor resource = findResource(path);
if (resource != null) {
return evaluateResourceAccess(request, response, resource, method);
}
// Default policy for unmapped resources
return evaluateDefaultPolicy(request, response);
}
private ResourceDescriptor findResource(String path) {
// Find best matching resource pattern
return resourceMap.entrySet().stream()
.filter(entry -> pathMatches(path, entry.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}
private boolean evaluateResourceAccess(HttpAuthzRequest request, HttpAuthzResponse response,
ResourceDescriptor resource, String method) {
TokenPrincipal principal = request.getPrincipal();
if (principal == null) {
response.sendError(401, "Authentication required");
return false;
}
AccessToken token = principal.getToken();
// Check required scopes for the method
Set<String> requiredScopes = resource.getRequiredScopes(method);
if (!hasRequiredScopes(token, requiredScopes)) {
response.sendError(403, "Insufficient scopes for " + method + " on " + resource.getName());
return false;
}
// Check resource-specific policies
if (resource.hasCustomPolicy()) {
PolicyDecision decision = resource.evaluateCustomPolicy(request);
if (!decision.isGranted()) {
response.sendError(403, decision.getReason());
return false;
}
}
return true;
}
private boolean hasRequiredScopes(AccessToken token, Set<String> requiredScopes) {
Set<String> tokenScopes = extractScopes(token);
return tokenScopes.containsAll(requiredScopes);
}
private Set<String> extractScopes(AccessToken token) {
String scopeClaim = (String) token.getOtherClaims().get("scope");
if (scopeClaim != null) {
return Set.of(scopeClaim.split(" "));
}
return Collections.emptySet();
}
private Map<String, ResourceDescriptor> buildResourceMap() {
Map<String, ResourceDescriptor> map = new HashMap<>();
// Define resources and their access requirements
map.put("/api/users/**", ResourceDescriptor.builder()
.name("User Management")
.scope("GET", "read:users")
.scope("POST", "create:users")
.scope("PUT", "update:users")
.scope("DELETE", "delete:users")
.customPolicy((request) -> {
// Custom policy: users can only access their own data
String path = request.getRelativePath();
String tokenSubject = request.getPrincipal().getToken().getSubject();
return path.contains("/" + tokenSubject + "/") ||
hasRole(request.getPrincipal().getToken(), "admin");
})
.build());
map.put("/api/documents/**", ResourceDescriptor.builder()
.name("Document Management")
.scope("GET", "read:documents")
.scope("POST", "create:documents")
.scope("PUT", "update:documents")
.scope("DELETE", "delete:documents")
.customPolicy((request) -> {
// Check document ownership or shared access
return checkDocumentAccess(request);
})
.build());
return map;
}
private boolean pathMatches(String path, String pattern) {
// Simple pattern matching (could use more sophisticated matching)
if (pattern.endsWith("/**")) {
String prefix = pattern.substring(0, pattern.length() - 3);
return path.startsWith(prefix);
}
return path.equals(pattern);
}
// Resource descriptor class
public static class ResourceDescriptor {
private final String name;
private final Map<String, Set<String>> methodScopes;
private final Function<HttpAuthzRequest, Boolean> customPolicy;
// Builder pattern implementation...
public Set<String> getRequiredScopes(String method) {
return methodScopes.getOrDefault(method.toUpperCase(), Collections.emptySet());
}
public boolean hasCustomPolicy() {
return customPolicy != null;
}
public PolicyDecision evaluateCustomPolicy(HttpAuthzRequest request) {
if (customPolicy != null) {
boolean allowed = customPolicy.apply(request);
return allowed ?
PolicyDecision.permit("Custom policy allows access") :
PolicyDecision.deny("Custom policy denies access");
}
return PolicyDecision.permit("No custom policy");
}
}
}// Claim-based authorization processor
public class ClaimBasedAuthorizationProcessor {
public AuthorizationDecision evaluate(HttpAuthzRequest request) {
TokenPrincipal principal = request.getPrincipal();
if (principal == null) {
return AuthorizationDecision.deny("No authentication token");
}
AccessToken token = principal.getToken();
Map<String, Object> claims = token.getOtherClaims();
// Department-based access control
String department = (String) claims.get("department");
String resource = request.getRelativePath();
if (resource.startsWith("/hr/") && !"HR".equals(department)) {
return AuthorizationDecision.deny("HR resources require HR department");
}
if (resource.startsWith("/finance/") && !"Finance".equals(department)) {
return AuthorizationDecision.deny("Finance resources require Finance department");
}
// Location-based access control
String userLocation = (String) claims.get("office_location");
String clientIp = request.getRemoteAddr();
if (!isLocationAllowed(userLocation, clientIp, resource)) {
return AuthorizationDecision.deny("Resource not accessible from current location");
}
// Time-based access control with user-specific rules
Integer maxHours = (Integer) claims.get("max_access_hours");
if (maxHours != null && isOutsideAllowedHours(maxHours)) {
return AuthorizationDecision.deny("Access outside allowed hours for user");
}
// Data classification access control
String clearanceLevel = (String) claims.get("clearance_level");
String resourceClassification = extractResourceClassification(resource);
if (!hasSufficientClearance(clearanceLevel, resourceClassification)) {
return AuthorizationDecision.deny("Insufficient clearance for resource classification");
}
return AuthorizationDecision.permit("All claim-based policies satisfied");
}
private boolean isLocationAllowed(String userLocation, String clientIp, String resource) {
// Implement location-based access logic
if ("REMOTE".equals(userLocation)) {
// Remote users have limited access
return !resource.contains("/sensitive/");
}
return true; // Office users have full access
}
private boolean isOutsideAllowedHours(int maxHours) {
int currentHour = LocalTime.now().getHour();
return currentHour >= maxHours;
}
private String extractResourceClassification(String resource) {
if (resource.contains("/classified/")) return "SECRET";
if (resource.contains("/confidential/")) return "CONFIDENTIAL";
if (resource.contains("/internal/")) return "INTERNAL";
return "PUBLIC";
}
private boolean hasSufficientClearance(String userClearance, String resourceClassification) {
Map<String, Integer> clearanceLevels = Map.of(
"PUBLIC", 0,
"INTERNAL", 1,
"CONFIDENTIAL", 2,
"SECRET", 3
);
int userLevel = clearanceLevels.getOrDefault(userClearance, 0);
int requiredLevel = clearanceLevels.getOrDefault(resourceClassification, 0);
return userLevel >= requiredLevel;
}
public static class AuthorizationDecision {
private final boolean granted;
private final String reason;
private AuthorizationDecision(boolean granted, String reason) {
this.granted = granted;
this.reason = reason;
}
public static AuthorizationDecision permit(String reason) {
return new AuthorizationDecision(true, reason);
}
public static AuthorizationDecision deny(String reason) {
return new AuthorizationDecision(false, reason);
}
public boolean isGranted() { return granted; }
public String getReason() { return reason; }
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-keycloak--keycloak-adapter-core