google-site-verification: google61fe8ba583a51912.html
JWT Validation with OPA in Java

Introduction to JWT and OPA

JSON Web Tokens (JWT) are widely used for authentication and authorization in modern applications. Open Policy Agent (OPA) is a general-purpose policy engine that can be used to enforce authorization policies, including JWT validation and claims verification.

Architecture Overview

+-------------+ +-------------+ +-------------+ | Client | --> | Java App | --> | OPA | | | | | | | | (JWT Token) | | (Validation)| | (Policy | +-------------+ +-------------+ | Engine) | +-------------+

Dependencies Setup

Maven Dependencies

<properties> <opa.client.version>1.0.0</opa.client.version> <jwt.version>0.11.5</jwt.version> <jackson.version>2.15.2</jackson.version> <spring.boot.version>3.1.0</spring.boot.version> </properties> <dependencies> <!-- OPA Client --> <dependency> <groupId>com.openpolicyagent</groupId> <artifactId>opa-client</artifactId> <version>${opa.client.version}</version> </dependency> <!-- JWT Library --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>${jwt.version}</version> </dependency> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring.boot.version}</version> <scope>test</scope> </dependency> </dependencies>

Core JWT Validation Components

JWT Token Model

package com.example.jwtopa.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; import java.util.List; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) public class JwtToken { private String token; private JwtHeader header; private JwtPayload payload; // Constructors public JwtToken() {} public JwtToken(String token) { this.token = token; } // Getters and Setters public String getToken() { return token; } public void setToken(String token) { this.token = token; } public JwtHeader getHeader() { return header; } public void setHeader(JwtHeader header) { this.header = header; } public JwtPayload getPayload() { return payload; } public void setPayload(JwtPayload payload) { this.payload = payload; } } @JsonIgnoreProperties(ignoreUnknown = true) class JwtHeader { private String alg; private String typ; private String kid; // Getters and Setters public String getAlg() { return alg; } public void setAlg(String alg) { this.alg = alg; } public String getTyp() { return typ; } public void setTyp(String typ) { this.typ = typ; } public String getKid() { return kid; } public void setKid(String kid) { this.kid = kid; } } @JsonIgnoreProperties(ignoreUnknown = true) class JwtPayload { private String iss; private String sub; private List<String> aud; private Instant exp; private Instant nbf; private Instant iat; private String jti; private Map<String, Object> claims; // Getters and Setters public String getIss() { return iss; } public void setIss(String iss) { this.iss = iss; } public String getSub() { return sub; } public void setSub(String sub) { this.sub = sub; } public List<String> getAud() { return aud; } public void setAud(List<String> aud) { this.aud = aud; } public Instant getExp() { return exp; } public void setExp(Instant exp) { this.exp = exp; } public Instant getNbf() { return nbf; } public void setNbf(Instant nbf) { this.nbf = nbf; } public Instant getIat() { return iat; } public void setIat(Instant iat) { this.iat = iat; } public String getJti() { return jti; } public void setJti(String jti) { this.jti = jti; } public Map<String, Object> getClaims() { return claims; } public void setClaims(Map<String, Object> claims) { this.claims = claims; } // Utility methods public boolean isExpired() { return exp != null && Instant.now().isAfter(exp); } public boolean isNotBeforeValid() { return nbf == null || Instant.now().isAfter(nbf); } public boolean hasClaim(String claim) { return claims != null && claims.containsKey(claim); } public Object getClaim(String claim) { return claims != null ? claims.get(claim) : null; } @SuppressWarnings("unchecked") public List<String> getRoles() { Object roles = getClaim("roles"); if (roles instanceof List) { return (List<String>) roles; } return List.of(); } @SuppressWarnings("unchecked") public List<String> getScopes() { Object scopes = getClaim("scope"); if (scopes instanceof String) { return List.of(((String) scopes).split(" ")); } else if (scopes instanceof List) { return (List<String>) scopes; } return List.of(); } }

OPA Client Implementation

package com.example.jwtopa.client; import com.example.jwtopa.model.JwtToken; import com.example.jwtopa.model.OpaResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Component public class OpaClient { private static final Logger logger = LoggerFactory.getLogger(OpaClient.class); private final String opaBaseUrl; private final ObjectMapper objectMapper; private final CloseableHttpClient httpClient; public OpaClient(@Value("${opa.base.url:http://localhost:8181}") String opaBaseUrl) { this.opaBaseUrl = opaBaseUrl; this.objectMapper = new ObjectMapper(); this.httpClient = HttpClients.createDefault(); } public OpaResponse validateJwt(String jwtToken, String path, Map<String, Object> input) { String opaUrl = opaBaseUrl + "/v1/" + path; Map<String, Object> requestBody = new HashMap<>(); requestBody.put("input", input); try { String requestJson = objectMapper.writeValueAsString(requestBody); logger.debug("Sending request to OPA: {}", requestJson); HttpPost httpPost = new HttpPost(opaUrl); httpPost.setHeader("Content-Type", "application/json"); httpPost.setEntity(new StringEntity(requestJson)); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { String responseBody = EntityUtils.toString(response.getEntity()); logger.debug("Received response from OPA: {}", responseBody); if (response.getCode() == 200) { return objectMapper.readValue(responseBody, OpaResponse.class); } else { logger.error("OPA request failed with status: {}", response.getCode()); throw new RuntimeException("OPA request failed with status: " + response.getCode()); } } } catch (JsonProcessingException e) { logger.error("Error serializing OPA request", e); throw new RuntimeException("Error serializing OPA request", e); } catch (IOException e) { logger.error("Error communicating with OPA", e); throw new RuntimeException("Error communicating with OPA", e); } } public OpaResponse validateJwtWithContext(String jwtToken, String method, String path, Map<String, Object> resourceAttributes) { Map<String, Object> input = new HashMap<>(); input.put("jwt", jwtToken); input.put("method", method); input.put("path", path); input.put("resource", resourceAttributes); return validateJwt(jwtToken, "authz/jwt/validate", input); } public OpaResponse validateJwtBasic(String jwtToken) { Map<String, Object> input = new HashMap<>(); input.put("jwt", jwtToken); return validateJwt(jwtToken, "authz/jwt/basic", input); } public void close() { try { httpClient.close(); } catch (IOException e) { logger.error("Error closing HTTP client", e); } } }

OPA Response Model

package com.example.jwtopa.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) public class OpaResponse { private boolean result; private Map<String, Object> decision; private String message; // Constructors public OpaResponse() {} public OpaResponse(boolean result) { this.result = result; } // Getters and Setters public boolean isResult() { return result; } public void setResult(boolean result) { this.result = result; } public Map<String, Object> getDecision() { return decision; } public void setDecision(Map<String, Object> decision) { this.decision = decision; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } // Utility methods public boolean isAllowed() { return result && decision != null && Boolean.TRUE.equals(decision.get("allowed")); } public String getReason() { return decision != null ? (String) decision.get("reason") : null; } @SuppressWarnings("unchecked") public Map<String, Object> getClaims() { return decision != null ? (Map<String, Object>) decision.get("claims") : null; } public Object getClaim(String claimName) { Map<String, Object> claims = getClaims(); return claims != null ? claims.get(claimName) : null; } }

JWT Parser and Validator

JWT Parser Service

package com.example.jwtopa.service; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import com.example.jwtopa.model.JwtPayload; import com.example.jwtopa.model.JwtToken; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.time.Instant; import java.util.Base64; import java.util.List; import java.util.Map; @Service public class JwtParserService { private static final Logger logger = LoggerFactory.getLogger(JwtParserService.class); private final ObjectMapper objectMapper; public JwtParserService() { this.objectMapper = new ObjectMapper(); } public JwtToken parseToken(String token) { try { DecodedJWT decodedJWT = JWT.decode(token); JwtToken jwtToken = new JwtToken(token); // Parse header com.example.jwtopa.model.JwtHeader header = new com.example.jwtopa.model.JwtHeader(); header.setAlg(decodedJWT.getAlgorithm()); header.setTyp(decodedJWT.getType()); header.setKid(decodedJWT.getKeyId()); jwtToken.setHeader(header); // Parse payload String payloadJson = new String(Base64.getUrlDecoder().decode(decodedJWT.getPayload())); Map<String, Object> claims = objectMapper.readValue(payloadJson, new TypeReference<Map<String, Object>>() {}); JwtPayload payload = new JwtPayload(); payload.setIss((String) claims.get("iss")); payload.setSub((String) claims.get("sub")); payload.setAud(parseAudience(claims.get("aud"))); payload.setExp(parseInstant(claims.get("exp"))); payload.setNbf(parseInstant(claims.get("nbf"))); payload.setIat(parseInstant(claims.get("iat"))); payload.setJti((String) claims.get("jti")); payload.setClaims(claims); jwtToken.setPayload(payload); return jwtToken; } catch (JWTDecodeException e) { logger.error("Failed to decode JWT token", e); throw new IllegalArgumentException("Invalid JWT token", e); } catch (Exception e) { logger.error("Error parsing JWT token", e); throw new RuntimeException("Error parsing JWT token", e); } } @SuppressWarnings("unchecked") private List<String> parseAudience(Object aud) { if (aud instanceof String) { return List.of((String) aud); } else if (aud instanceof List) { return (List<String>) aud; } return List.of(); } private Instant parseInstant(Object timestamp) { if (timestamp instanceof Integer) { return Instant.ofEpochSecond((Integer) timestamp); } else if (timestamp instanceof Long) { return Instant.ofEpochSecond((Long) timestamp); } return null; } public boolean isTokenExpired(JwtToken jwtToken) { return jwtToken.getPayload() != null && jwtToken.getPayload().isExpired(); } public boolean isTokenActive(JwtToken jwtToken) { JwtPayload payload = jwtToken.getPayload(); if (payload == null) return false; return !payload.isExpired() && payload.isNotBeforeValid(); } public void validateTokenStructure(String token) { if (token == null || token.trim().isEmpty()) { throw new IllegalArgumentException("Token cannot be null or empty"); } String[] parts = token.split("\\."); if (parts.length != 3) { throw new IllegalArgumentException("Invalid JWT token structure"); } } }

OPA Policy Integration

Policy Evaluation Service

package com.example.jwtopa.service; import com.example.jwtopa.client.OpaClient; import com.example.jwtopa.model.JwtToken; import com.example.jwtopa.model.OpaResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class OpaPolicyService { private static final Logger logger = LoggerFactory.getLogger(OpaPolicyService.class); private final OpaClient opaClient; private final JwtParserService jwtParserService; public OpaPolicyService(OpaClient opaClient, JwtParserService jwtParserService) { this.opaClient = opaClient; this.jwtParserService = jwtParserService; } public AuthorizationResult validateToken(String jwtToken) { try { // Basic structural validation jwtParserService.validateTokenStructure(jwtToken); // Parse token to extract basic information JwtToken parsedToken = jwtParserService.parseToken(jwtToken); // Check if token is expired locally (fast fail) if (jwtParserService.isTokenExpired(parsedToken)) { return AuthorizationResult.denied("Token has expired"); } // Validate with OPA OpaResponse opaResponse = opaClient.validateJwtBasic(jwtToken); if (opaResponse.isAllowed()) { return AuthorizationResult.allowed(opaResponse.getClaims()); } else { return AuthorizationResult.denied(opaResponse.getReason()); } } catch (IllegalArgumentException e) { logger.warn("Invalid token structure: {}", e.getMessage()); return AuthorizationResult.denied("Invalid token structure: " + e.getMessage()); } catch (Exception e) { logger.error("Error validating token with OPA", e); return AuthorizationResult.denied("Internal validation error"); } } public AuthorizationResult authorizeRequest(String jwtToken, String method, String path, Map<String, Object> resourceAttributes) { try { // Basic token validation first jwtParserService.validateTokenStructure(jwtToken); // Validate with OPA including request context OpaResponse opaResponse = opaClient.validateJwtWithContext( jwtToken, method, path, resourceAttributes); if (opaResponse.isAllowed()) { return AuthorizationResult.allowed(opaResponse.getClaims()); } else { return AuthorizationResult.denied(opaResponse.getReason()); } } catch (IllegalArgumentException e) { logger.warn("Invalid token structure: {}", e.getMessage()); return AuthorizationResult.denied("Invalid token structure: " + e.getMessage()); } catch (Exception e) { logger.error("Error authorizing request with OPA", e); return AuthorizationResult.denied("Internal authorization error"); } } public AuthorizationResult authorizeWithCustomPolicy(String jwtToken, String policyPath, Map<String, Object> additionalInput) { try { jwtParserService.validateTokenStructure(jwtToken); Map<String, Object> input = new HashMap<>(); input.put("jwt", jwtToken); if (additionalInput != null) { input.putAll(additionalInput); } OpaResponse opaResponse = opaClient.validateJwt(jwtToken, policyPath, input); if (opaResponse.isAllowed()) { return AuthorizationResult.allowed(opaResponse.getClaims()); } else { return AuthorizationResult.denied(opaResponse.getReason()); } } catch (Exception e) { logger.error("Error executing custom policy: {}", policyPath, e); return AuthorizationResult.denied("Policy evaluation failed"); } } public static class AuthorizationResult { private final boolean allowed; private final String reason; private final Map<String, Object> claims; private AuthorizationResult(boolean allowed, String reason, Map<String, Object> claims) { this.allowed = allowed; this.reason = reason; this.claims = claims; } public static AuthorizationResult allowed(Map<String, Object> claims) { return new AuthorizationResult(true, "Access granted", claims); } public static AuthorizationResult denied(String reason) { return new AuthorizationResult(false, reason, null); } // Getters public boolean isAllowed() { return allowed; } public String getReason() { return reason; } public Map<String, Object> getClaims() { return claims; } @SuppressWarnings("unchecked") public List<String> getRoles() { return claims != null && claims.containsKey("roles") ? (List<String>) claims.get("roles") : List.of(); } public String getSubject() { return claims != null ? (String) claims.get("sub") : null; } } }

Spring Security Integration

JWT Authentication Filter

package com.example.jwtopa.security; import com.example.jwtopa.service.OpaPolicyService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.List; import java.util.stream.Collectors; public class JwtOpaAuthenticationFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(JwtOpaAuthenticationFilter.class); private final OpaPolicyService opaPolicyService; private static final String AUTH_HEADER = "Authorization"; private static final String BEARER_PREFIX = "Bearer "; public JwtOpaAuthenticationFilter(OpaPolicyService opaPolicyService) { this.opaPolicyService = opaPolicyService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader(AUTH_HEADER); if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { filterChain.doFilter(request, response); return; } String jwtToken = authHeader.substring(BEARER_PREFIX.length()); try { // Validate token with OPA OpaPolicyService.AuthorizationResult result = opaPolicyService.authorizeRequest( jwtToken, request.getMethod(), request.getRequestURI(), extractResourceAttributes(request) ); if (result.isAllowed()) { // Create authentication object List<SimpleGrantedAuthority> authorities = result.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toList()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( result.getSubject(), null, authorities ); // Set authentication in security context SecurityContextHolder.getContext().setAuthentication(authentication); logger.debug("Authenticated user: {} with roles: {}", result.getSubject(), result.getRoles()); } else { logger.warn("Access denied for token. Reason: {}", result.getReason()); SecurityContextHolder.clearContext(); } } catch (Exception e) { logger.error("Error processing JWT token", e); SecurityContextHolder.clearContext(); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token"); return; } filterChain.doFilter(request, response); } private java.util.Map<String, Object> extractResourceAttributes(HttpServletRequest request) { java.util.Map<String, Object> attributes = new java.util.HashMap<>(); attributes.put("ip", getClientIpAddress(request)); attributes.put("userAgent", request.getHeader("User-Agent")); attributes.put("queryParams", request.getParameterMap()); return attributes; } private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } }

Security Configuration

package com.example.jwtopa.config; import com.example.jwtopa.security.JwtOpaAuthenticationFilter; import com.example.jwtopa.service.OpaPolicyService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { private final OpaPolicyService opaPolicyService; public SecurityConfig(OpaPolicyService opaPolicyService) { this.opaPolicyService = opaPolicyService; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/user/**").hasRole("USER") .anyRequest().authenticated() ) .addFilterBefore( new JwtOpaAuthenticationFilter(opaPolicyService), UsernamePasswordAuthenticationFilter.class ); return http.build(); } }

REST Controller Examples

Authentication Controller

package com.example.jwtopa.controller; import com.example.jwtopa.model.JwtToken; import com.example.jwtopa.service.JwtParserService; import com.example.jwtopa.service.OpaPolicyService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/auth") public class AuthController { private static final Logger logger = LoggerFactory.getLogger(AuthController.class); private final OpaPolicyService opaPolicyService; private final JwtParserService jwtParserService; public AuthController(OpaPolicyService opaPolicyService, JwtParserService jwtParserService) { this.opaPolicyService = opaPolicyService; this.jwtParserService = jwtParserService; } @PostMapping("/validate") public ResponseEntity<Map<String, Object>> validateToken(@RequestBody Map<String, String> request) { String token = request.get("token"); if (token == null || token.trim().isEmpty()) { return ResponseEntity.badRequest().body(Map.of( "valid", false, "message", "Token is required" )); } OpaPolicyService.AuthorizationResult result = opaPolicyService.validateToken(token); return ResponseEntity.ok(Map.of( "valid", result.isAllowed(), "message", result.getReason(), "subject", result.getSubject(), "roles", result.getRoles() )); } @PostMapping("/parse") public ResponseEntity<?> parseToken(@RequestBody Map<String, String> request) { String token = request.get("token"); try { JwtToken jwtToken = jwtParserService.parseToken(token); return ResponseEntity.ok(jwtToken); } catch (Exception e) { return ResponseEntity.badRequest().body(Map.of( "error", "Failed to parse token", "message", e.getMessage() )); } } @GetMapping("/userinfo") public ResponseEntity<Map<String, Object>> getUserInfo(@RequestHeader("Authorization") String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { return ResponseEntity.badRequest().body(Map.of( "error", "Missing or invalid Authorization header" )); } String token = authHeader.substring(7); OpaPolicyService.AuthorizationResult result = opaPolicyService.validateToken(token); if (!result.isAllowed()) { return ResponseEntity.status(401).body(Map.of( "error", "Invalid token", "message", result.getReason() )); } return ResponseEntity.ok(Map.of( "subject", result.getSubject(), "roles", result.getRoles(), "claims", result.getClaims() )); } }

Protected Resource Controller

package com.example.jwtopa.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api") public class ResourceController { private static final Logger logger = LoggerFactory.getLogger(ResourceController.class); @GetMapping("/public/hello") public ResponseEntity<Map<String, String>> publicHello() { return ResponseEntity.ok(Map.of("message", "Hello from public endpoint!")); } @GetMapping("/user/profile") public ResponseEntity<Map<String, Object>> userProfile(Authentication authentication) { String username = authentication.getName(); logger.info("Accessing user profile for: {}", username); return ResponseEntity.ok(Map.of( "message", "Hello " + username + "!", "profile", Map.of( "username", username, "role", "USER" ) )); } @GetMapping("/admin/dashboard") public ResponseEntity<Map<String, Object>> adminDashboard(Authentication authentication) { String username = authentication.getName(); logger.info("Accessing admin dashboard for: {}", username); return ResponseEntity.ok(Map.of( "message", "Welcome to admin dashboard, " + username + "!", "stats", Map.of( "users", 1500, "revenue", 50000, "activeSessions", 42 ) )); } @PostMapping("/user/data") public ResponseEntity<Map<String, Object>> createData( @RequestBody Map<String, Object> data, Authentication authentication) { String username = authentication.getName(); logger.info("User {} creating data: {}", username, data); return ResponseEntity.ok(Map.of( "message", "Data created successfully", "createdBy", username, "data", data )); } }

OPA Policy Examples

Basic JWT Validation Policy

package authz.jwt.basic import future.keywords.in # Default deny default allowed := false # JWT validation result allowed := { "allowed": is_token_valid, "reason": reason, "claims": payload } { # Parse and validate JWT io.jwt.verify_rs256(input.jwt) [header, payload, signature] := io.jwt.decode(input.jwt) # Check expiration now := time.now_ns() / 1000000000 exp := payload.exp exp > now # Check not before nbf := payload.nbf nbf <= now is_token_valid := true reason := "Token is valid" } # Token expired allowed := { "allowed": false, "reason": "Token has expired" } { [header, payload, signature] := io.jwt.decode(input.jwt) now := time.now_ns() / 1000000000 exp := payload.exp exp <= now } # Token not yet active allowed := { "allowed": false, "reason": "Token not yet active" } { [header, payload, signature] := io.jwt.decode(input.jwt) now := time.now_ns() / 1000000000 nbf := payload.nbf nbf > now } # Invalid token allowed := { "allowed": false, "reason": "Invalid token signature or format" } { not io.jwt.verify_rs256(input.jwt) }

Advanced Authorization Policy

package authz.jwt.validate import future.keywords.in # Default deny default allowed := false # Main authorization logic allowed := { "allowed": decision, "reason": reason, "claims": payload } { # Parse JWT [header, payload, signature] := io.jwt.decode(input.jwt) # Validate token is_token_valid := check_token_validity(header, payload, signature) # Extract request context method := input.method path := input.path resource := input.resource # Make authorization decision decision := is_token_valid and check_permissions(method, path, payload, resource) reason := decision then "Access granted" else "Access denied" } # Token validation checks check_token_validity(header, payload, signature) := valid { # Verify signature io.jwt.verify_rs256(input.jwt) # Check expiration now := time.now_ns() / 1000000000 exp := payload.exp exp > now # Check issuer payload.iss == "https://auth.mycompany.com" # Check audience "api.mycompany.com" in payload.aud valid := true } # Permission checks check_permissions(method, path, payload, resource) := allowed { # Extract user roles roles := payload.roles # Check based on path patterns and roles allowed := any_rule_allows(method, path, roles, resource) } # Rule for admin endpoints any_rule_allows(method, path, roles, resource) { startswith(path, "/api/admin") "admin" in roles } # Rule for user endpoints any_rule_allows(method, path, roles, resource) { startswith(path, "/api/user") "user" in roles } # Rule for public endpoints any_rule_allows(method, path, roles, resource) { startswith(path, "/api/public") } # Rule for specific resource ownership any_rule_allows(method, path, roles, resource) { startswith(path, "/api/data/") method == "GET" resource.ownerId == payload.sub } # Get user roles from token get_user_roles(payload) := roles { roles := payload.roles } else := [] { true }

Testing

Unit Tests

package com.example.jwtopa.service; import com.example.jwtopa.client.OpaClient; import com.example.jwtopa.model.OpaResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class OpaPolicyServiceTest { @Mock private OpaClient opaClient; @Mock private JwtParserService jwtParserService; private OpaPolicyService opaPolicyService; @BeforeEach void setUp() { opaPolicyService = new OpaPolicyService(opaClient, jwtParserService); } @Test void validateToken_ValidToken_ReturnsAllowed() { // Given String validToken = "valid.jwt.token"; OpaResponse opaResponse = new OpaResponse(true); opaResponse.setDecision(Map.of("allowed", true, "reason", "Token is valid")); when(opaClient.validateJwtBasic(validToken)).thenReturn(opaResponse); // When OpaPolicyService.AuthorizationResult result = opaPolicyService.validateToken(validToken); // Then assertTrue(result.isAllowed()); assertEquals("Token is valid", result.getReason()); } @Test void validateToken_InvalidToken_ReturnsDenied() { // Given String invalidToken = "invalid.jwt.token"; OpaResponse opaResponse = new OpaResponse(true); opaResponse.setDecision(Map.of("allowed", false, "reason", "Invalid signature")); when(opaClient.validateJwtBasic(invalidToken)).thenReturn(opaResponse); // When OpaPolicyService.AuthorizationResult result = opaPolicyService.validateToken(invalidToken); // Then assertFalse(result.isAllowed()); assertEquals("Invalid signature", result.getReason()); } @Test void authorizeRequest_AdminAccess_ReturnsAllowed() { // Given String token = "admin.jwt.token"; String method = "GET"; String path = "/api/admin/dashboard"; Map<String, Object> resourceAttributes = new HashMap<>(); OpaResponse opaResponse = new OpaResponse(true); opaResponse.setDecision(Map.of( "allowed", true, "reason", "Access granted", "claims", Map.of("sub", "admin-user", "roles", java.util.List.of("admin")) )); when(opaClient.validateJwtWithContext(eq(token), eq(method), eq(path), anyMap())) .thenReturn(opaResponse); // When OpaPolicyService.AuthorizationResult result = opaPolicyService.authorizeRequest(token, method, path, resourceAttributes); // Then assertTrue(result.isAllowed()); assertEquals("Access granted", result.getReason()); assertEquals("admin-user", result.getSubject()); assertTrue(result.getRoles().contains("admin")); } }

Integration Test

package com.example.jwtopa.integration; import com.example.jwtopa.controller.AuthController; import com.example.jwtopa.service.OpaPolicyService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.util.Map; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(AuthController.class) class AuthControllerIntegrationTest { @Autowired private MockMvc mockMvc; @MockBean private OpaPolicyService opaPolicyService; @MockBean private JwtParserService jwtParserService; @Test void validateToken_ValidToken_ReturnsSuccess() throws Exception { // Given String validToken = "valid.jwt.token"; OpaPolicyService.AuthorizationResult result = OpaPolicyService.AuthorizationResult.allowed( Map.of("sub", "test-user", "roles", java.util.List.of("user")) ); when(opaPolicyService.validateToken(anyString())).thenReturn(result); // When & Then mockMvc.perform(post("/api/auth/validate") .contentType(MediaType.APPLICATION_JSON) .content("{\"token\": \"" + validToken + "\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.valid").value(true)) .andExpect(jsonPath("$.subject").value("test-user")) .andExpect(jsonPath("$.roles[0]").value("user")); } }

Configuration

Application Properties

# application.yml server: port: 8080 opa: base: url: http://localhost:8181 logging: level: com.example.jwtopa: DEBUG spring: security: oauth2: resourceserver: jwt: issuer-uri: https://auth.mycompany.com app: security: jwt: issuer: https://auth.mycompany.com audience: api.mycompany.com require-https: true

Error Handling

Global Exception Handler

package com.example.jwtopa.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<Map<String, Object>> handleAccessDenied(AccessDeniedException e) { logger.warn("Access denied: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of( "error", "access_denied", "message", "You don't have permission to access this resource", "details", e.getMessage() )); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<Map<String, Object>> handleIllegalArgument(IllegalArgumentException e) { logger.warn("Invalid request: {}", e.getMessage()); return ResponseEntity.badRequest().body(Map.of( "error", "invalid_request", "message", e.getMessage() )); } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleGenericException(Exception e) { logger.error("Internal server error", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of( "error", "internal_error", "message", "An internal server error occurred" )); } }

Best Practices

1. Token Validation Best Practices

package com.example.jwtopa.bestpractices; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; public class TokenValidationBestPractices { private static final Logger logger = LoggerFactory.getLogger(TokenValidationBestPractices.class); // Cache OPA decisions for valid tokens to reduce load public static class TokenCache { private final java.util.Map<String, CachedDecision> cache = new java.util.concurrent.ConcurrentHashMap<>(); private final long ttlMillis; public TokenCache(long ttl, TimeUnit unit) { this.ttlMillis = unit.toMillis(ttl); } public void put(String token, boolean allowed, String reason) { cache.put(token, new CachedDecision(allowed, reason, System.currentTimeMillis())); } public CachedDecision get(String token) { CachedDecision decision = cache.get(token); if (decision != null && System.currentTimeMillis() - decision.timestamp > ttlMillis) { cache.remove(token); return null; } return decision; } public void cleanup() { long now = System.currentTimeMillis(); cache.entrySet().removeIf(entry -> now - entry.getValue().timestamp > ttlMillis); } private static class CachedDecision { final boolean allowed; final String reason; final long timestamp; CachedDecision(boolean allowed, String reason, long timestamp) { this.allowed = allowed; this.reason = reason; this.timestamp = timestamp; } } } // Rate limiting for token validation public static class RateLimiter { private final java.util.Map<String, RateLimit> limits = new java.util.concurrent.ConcurrentHashMap<>(); private final int maxRequests; private final long windowMillis; public RateLimiter(int maxRequests, long window, TimeUnit unit) { this.maxRequests = maxRequests; this.windowMillis = unit.toMillis(window); } public boolean allow(String key) { long now = System.currentTimeMillis(); RateLimit limit = limits.computeIfAbsent(key, k -> new RateLimit()); synchronized (limit) { if (now - limit.windowStart > windowMillis) { limit.count = 1; limit.windowStart = now; return true; } else if (limit.count < maxRequests) { limit.count++; return true; } else { return false; } } } private static class RateLimit { int count = 0; long windowStart = System.currentTimeMillis(); } } }

Conclusion

JWT validation with OPA in Java provides a robust, flexible, and maintainable approach to authentication and authorization. Key benefits include:

  1. Separation of Concerns: Authorization logic is separated from application code
  2. Policy as Code: Authorization policies are version-controlled and testable
  3. Flexibility: OPA policies can be updated without redeploying the application
  4. Centralized Management: Consistent authorization across multiple services
  5. Auditability: Clear, readable policies that can be reviewed and audited

This approach enables fine-grained access control while maintaining security best practices and providing excellent developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper