Java SDK
Goal
This guide walks you through installing, configuring, and using the Keymate Java SDK to perform authorization checks from JVM-based applications. By the end, you will be able to check permissions, list all granted permissions, and retrieve organization context — both programmatically and declaratively with Spring Boot annotations.
Audience
- Backend developers building Java or Kotlin applications that need fine-grained authorization
- Spring Boot developers looking for annotation-based permission enforcement
- Teams integrating with the Keymate Access Gateway
Prerequisites
- Java 8 or later (Java 17+ for Spring Boot 3.x integration, Java 8/11 for Spring Boot 2.x)
- A running Keymate environment with Access Gateway accessible over HTTP
- A registered Keycloak client identifier (e.g.,
demo-spa) - Maven or Gradle build tool
Before You Start
The Java SDK acts as a Policy Enforcement Point (PEP) — the component that enforces authorization decisions within your application. It communicates with the Access Gateway, which serves as the PDP Proxy and Edge Orchestrator handling token validation, access rule evaluation, and decision caching on behalf of all enforcers (PEPs).
The SDK supports two authorization schemes: Bearer tokens and DPoP (RFC 9449) sender-constrained tokens. Both are handled transparently by the SDK.
Module Structure
The SDK is organized into the following modules:
| Module | Purpose | JDK Requirement |
|---|---|---|
keymate-sdk-core | Standalone SDK for any Java application | Java 8+ |
keymate-sdk-spring | Spring Boot 3.x integration | Java 17+ |
keymate-sdk-spring5 | Spring Boot 2.x integration | Java 8+ |
Worked Example
Throughout this guide, the examples use a demo scenario: a frontend SPA client (demo-spa) needs to check whether the authenticated user can access resources on a backend API (demo-api). The SDK enforces these permission checks by communicating with the Access Gateway.
Steps
1. Add the SDK dependency
Add the appropriate module to your project based on your framework and JDK version.
Spring Boot 3.x (Jakarta Servlet, JDK 17+):
<dependency>
<groupId>io.keymate</groupId>
<artifactId>keymate-sdk-spring</artifactId>
<version>${keymate.sdk.version}</version>
</dependency>
Spring Boot 2.x (javax Servlet, JDK 8+):
<dependency>
<groupId>io.keymate</groupId>
<artifactId>keymate-sdk-spring5</artifactId>
<version>${keymate.sdk.version}</version>
</dependency>
Core only (no Spring, JDK 8+):
<dependency>
<groupId>io.keymate</groupId>
<artifactId>keymate-sdk-core</artifactId>
<version>${keymate.sdk.version}</version>
</dependency>
Both Spring modules pull in the core and shared-annotation modules as transitive dependencies. You do not need to add them separately.
2. Configure the SDK
Spring Boot Configuration
Add the following configuration to your application.yml:
keymate:
access-gateway:
base-url: https://gateway.example.com
client-id: demo-spa
timeout: 30000
retry-count: 3
retry-interval: 1000
| Property | Required | Default | Description |
|---|---|---|---|
base-url | Yes | — | Base URL of the Access Gateway |
client-id | Yes | — | Keycloak client identifier |
timeout | No | 30000 | Request timeout in milliseconds |
retry-count | No | 3 | Number of retry attempts on failure |
retry-interval | No | 1000 | Interval between retries in milliseconds |
Spring Boot auto-configuration creates an AccessGatewayService bean automatically. No additional bean registration is needed.
Programmatic Configuration
Use the builder to create a configuration instance:
KeymateSdkConfiguration config = new KeymateSdkConfiguration.Builder()
.setAccessGatewayUrl("https://gateway.example.com")
.setClientId("demo-spa")
.setTimeout(Duration.ofSeconds(30))
.setRetryCount(3)
.setRetryIntervalMillis(1000)
.build();
Or use the factory for a quick setup with defaults:
AccessGatewayService service = AccessGatewayFactory.createService(
"https://gateway.example.com",
"demo-spa"
);
3. Check permissions
The SDK provides two approaches for permission checking: annotation-based (Spring Boot) and programmatic (any Java application).
Annotation-Based (Spring Boot)
Use @CheckPermission on controller methods. The aspect intercepts the call, extracts the access token from the Authorization header (Bearer or DPoP), and checks permissions against the Access Gateway before the method executes.
@GetMapping
@CheckPermission(resources = @ResourceDef(name = "users", scopes = {"list"}))
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
Check multiple resources in a single call:
@GetMapping
@CheckPermission(resources = {
@ResourceDef(name = "users", scopes = {"list"}),
@ResourceDef(name = "guests", scopes = {"list"})
})
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
Pass context attributes from the request body:
@PostMapping
@CheckPermission(
resources = @ResourceDef(name = "users", scopes = {"create"}),
context = @Context(attributes = {
@ContextAttribute(
attributeName = "username",
type = ValueSource.REQUEST_BODY,
bodyParameter = @RequestBodyParameter(path = "/username")
),
@ContextAttribute(
attributeName = "email",
type = ValueSource.REQUEST_BODY,
bodyParameter = @RequestBodyParameter(path = "/email")
)
})
)
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
Auto-Inference
When @CheckPermission is used without explicit resource definitions, the SDK automatically infers the resource name, scope, and context from your Spring MVC annotations. This reduces boilerplate — you annotate your controller method, and the SDK determines what to check.
Resource name — resolved using the following priority (first match wins):
| Priority | Source | Result |
|---|---|---|
| 1 | Method-level mapping path containing a resource or resources segment | First non-variable segment after the keyword (e.g., @GetMapping("/resources/users/{id}") → users) |
| 2 | Class-level @RequestMapping path | Last non-variable segment (e.g., @RequestMapping("/api/v1/users") → users) |
| 3 | Controller class name | Suffix (Controller / Resource) stripped and converted to kebab-case (e.g., EmployeeDataController → employee-data) |
Scope — mapped from the HTTP method annotation: @GetMapping → read, @PostMapping → create, @PutMapping / @PatchMapping → update, @DeleteMapping → delete.
Context attributes — extracted from @RequestBody fields when no explicit @Context is defined.
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@PostMapping
@CheckPermission
public ResponseEntity<User> createUser(@RequestBody User user) {
// Resource: "users" (from class-level path), Scope: "create" (from @PostMapping)
// Context: request body fields are included automatically
return ResponseEntity.ok(userService.createUser(user));
}
@PutMapping("/{id}/resource/orders")
@CheckPermission
public ResponseEntity<Order> updateOrder(@PathVariable String id,
@RequestBody Order order) {
// Resource: "orders" (from method-level "resource" keyword), Scope: "update"
return ResponseEntity.ok(orderService.updateOrder(id, order));
}
}
Programmatic
Inject or create an AccessGatewayService instance and call methods directly:
@RestController
@RequestMapping("/api/permissions")
public class PermissionController {
private final AccessGatewayService accessGatewayService;
public PermissionController(AccessGatewayService accessGatewayService) {
this.accessGatewayService = accessGatewayService;
}
@GetMapping("/check")
public ResponseEntity<Boolean> checkPermission(
@RequestHeader("Authorization") String authorization,
@RequestParam String resource,
@RequestParam String scope) {
String token = authorization.replaceFirst("(?i)^(Bearer|DPoP)\\s+", "");
List<Resource> resources = List.of(
new Resource(resource, new String[]{scope})
);
boolean granted = accessGatewayService.checkPermission(token, resources, null);
return ResponseEntity.ok(granted);
}
}
4. List permissions
List all permissions the authenticated user has access to.
Annotation-Based
Use @ListPermissions on a controller method. The aspect sends a wildcard resource request to the Access Gateway and returns the result. The method body is not executed — the aspect generates the response.
@GetMapping("/my-permissions")
@ListPermissions
public PermissionResult getAllPermissions() {
throw new UnsupportedOperationException("Intercepted by @ListPermissions aspect");
}
The return type determines what you receive:
PermissionResult— full result including status and all permissions (granted and denied)List<ResourcePermission>— only granted permissions
Programmatic
PermissionResult result = accessGatewayService.listPermissions(accessToken);
// Check overall status
String status = result.getStatus(); // "GRANT", "DENY", or "ERROR"
// Iterate individual permissions
for (ResourcePermission perm : result.getPermissions()) {
String resource = perm.getResource();
String scope = perm.getScope();
boolean granted = perm.isGranted();
}
// Get only granted permissions
List<ResourcePermission> granted = result.getGrantedPermissions();
5. Retrieve organization context
Retrieve the authenticated user's organization hierarchy (tenants and departments).
Organization org = accessGatewayService.getOrganizationContext(accessToken);
String userId = org.getUserId();
for (Tenant tenant : org.getTenants()) {
String tenantId = tenant.getTenantId();
String tenantName = tenant.getTenantName();
for (Department dept : tenant.getDepartments()) {
String deptId = dept.getDepartmentId();
String deptName = dept.getDepartmentName();
}
}
6. Handle responses and errors
Permission check results
checkPermission Return | Meaning |
|---|---|
true | All requested resources are granted |
false | One or more resources are denied |
Spring exception handling
The Spring module throws specific exceptions that map to HTTP status codes:
| Exception | HTTP Status | Cause |
|---|---|---|
UnauthorizedException | 401 | Token is invalid, expired, or missing |
ForbiddenException | 403 | Permission denied or Access Gateway error |
ForbiddenException includes error details from the Access Gateway:
try {
// permission check via service or annotation
} catch (ForbiddenException e) {
String errorCode = e.getErrorCode(); // e.g., "TOKEN_EXCHANGE_FAILED"
int gatewayStatus = e.getGatewayStatus(); // e.g., 403
String message = e.getMessage(); // e.g., "Permission denied"
}
The SDK includes a built-in @ControllerAdvice exception handler (KeymateExceptionHandler) that automatically maps these exceptions to structured JSON responses.
Core SDK exceptions
When using keymate-sdk-core without Spring, handle these exceptions:
| Exception | Cause |
|---|---|
HttpErrorException | HTTP error from the Access Gateway (4xx, 5xx) |
RetryExhaustedException | All retry attempts failed |
KeymateSdkException | General SDK error (parent class) |
Error codes
The SDK defines error codes for validation and communication failures:
| Error Code | Description |
|---|---|
INVALID_ACCESS_TOKEN | Access token is null or blank |
INVALID_RESOURCE_NAME | Resource name contains invalid characters |
INVALID_RESOURCE_SCOPES | Resource scopes are missing or invalid |
INVALID_CLIENT_ID | Client ID is null or blank |
HTTP_CLIENT_ERROR | Access Gateway returned 4xx |
HTTP_SERVER_ERROR | Access Gateway returned 5xx |
RETRY_EXHAUSTED | All retries exhausted |
JSON_PARSE_ERROR | Failed to parse Access Gateway response |
7. Use async operations
Every operation has a CompletableFuture-based async variant:
CompletableFuture<Boolean> future = accessGatewayService.checkPermissionAsync(
accessToken, resources, context
);
future.thenAccept(granted -> {
if (granted) {
// proceed
}
});
CompletableFuture<Organization> future =
accessGatewayService.getOrganizationContextAsync(accessToken);
8. Use DPoP tokens (optional)
When using DPoP (RFC 9449) sender-constrained tokens, set the request metadata before calling the SDK:
try {
KeymateRequestMetadata.set("/api/users/123", "GET", dpopProofJwt);
boolean granted = accessGatewayService.checkPermission(dpopToken, resources, null);
} finally {
KeymateRequestMetadata.clear(); // prevent thread-local leaks
}
In Spring Boot applications, the aspect extracts Keymate-Target-URI, Keymate-Target-Method, and DPoP headers from the incoming request automatically.
Always call KeymateRequestMetadata.clear() in a finally block when using programmatic DPoP. Failing to clear the ThreadLocal can cause metadata leaks across requests in thread-pooled environments.
Validation Scenario
Scenario
A Spring Boot 3.x application (demo-spa) checks whether the authenticated user has list permission on the users resource.
Expected Result
- If the user has the permission, the controller method executes and returns
200 OK. - If the user lacks the permission, the SDK throws
ForbiddenExceptionand returns403 Forbidden. - If the token is invalid or expired, the SDK throws
UnauthorizedExceptionand returns401 Unauthorized.
How to Verify
- API evidence: Send a
GET /api/usersrequest with a valid access token (Bearer or DPoP). A200response confirms the permission check passed. - API evidence: Send the same request with an invalid token. A
401response confirms token validation. - API evidence: Send the request with a valid token for a user without the
users:listpermission. A403response confirms enforcement. - Logs / traces: Check your application logs for SDK permission check entries. Verify that the
Keymate-DecisionandKeymate-Decision-Cacheresponse headers are present in the Access Gateway response. - Audit evidence: Verify that the permission check decision appears in the Keymate audit log with the correct user, resource (
users), and scope (list).
Troubleshooting
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Connection timeout to Access Gateway | base-url is incorrect or the gateway is down | Verify the base-url is reachable from your application. Check network policies and firewall rules. |
UnauthorizedException on every request | Access token is expired or invalid | Confirm that the access token is valid and not expired. Verify the client-id matches the Keycloak client that issued the token. |
ForbiddenException with CLIENT_ID_MISMATCH | SDK client-id does not match token claims | Verify the client-id in SDK configuration matches the client_id or azp claim in the access token. |
| DPoP proof validation fails | Header mismatch or clock skew | Verify that Keymate-Target-URI and Keymate-Target-Method match the DPoP proof's htu and htm claims. |
RetryExhaustedException | Access Gateway is not responding after all retries | Check gateway health and increase timeout or retry-count if needed. |
| Spring annotation not intercepting | AOP not enabled or module missing from classpath | Confirm that AOP is enabled in your Spring Boot application and the keymate-sdk-spring (or keymate-sdk-spring5) module is on the classpath. |
Next Steps
- Review the Access Gateway Overview to understand how permission checks are processed
- Learn about the Enforcement Pipeline to understand how permission check requests are processed
- Explore Authority Integration & Token Mediation for details on token exchange and DPoP binding
Related Docs
Access Gateway Overview
Gateway architecture, endpoints, and technology stack.
Enforcement Pipeline
Permission check request processing stages and response headers.
Organization Context Endpoint
Organization context API with negative caching.
Sender-Constrained Tokens & DPoP
RFC 9449 DPoP specification and security model.
Which Java version does the Keymate SDK require?
The core SDK requires Java 8 or later. For Spring Boot 3.x integration, Java 17 or later is required. Spring Boot 2.x integration supports Java 8 and later.
Does the SDK support DPoP sender-constrained tokens?
Yes. The SDK supports both Bearer and DPoP (RFC 9449) authorization schemes. In Spring Boot applications, DPoP proof headers are extracted automatically. In programmatic usage, set the request metadata via KeymateRequestMetadata before calling the SDK.
Do I need to configure resource names and scopes for every endpoint?
No. When using @CheckPermission in Spring Boot, the SDK can automatically infer the resource name from your request mapping path and the scope from the HTTP method annotation. You only need explicit definitions when the defaults do not match your authorization model.
How does the SDK handle Access Gateway failures?
The SDK retries failed requests according to the configured retry-count and retry-interval. If all retries are exhausted, a RetryExhaustedException is thrown. In Spring Boot, this maps to a 403 Forbidden response.