Skip to main content

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:

ModulePurposeJDK Requirement
keymate-sdk-coreStandalone SDK for any Java applicationJava 8+
keymate-sdk-springSpring Boot 3.x integrationJava 17+
keymate-sdk-spring5Spring Boot 2.x integrationJava 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+):

pom.xml
<dependency>
<groupId>io.keymate</groupId>
<artifactId>keymate-sdk-spring</artifactId>
<version>${keymate.sdk.version}</version>
</dependency>

Spring Boot 2.x (javax Servlet, JDK 8+):

pom.xml
<dependency>
<groupId>io.keymate</groupId>
<artifactId>keymate-sdk-spring5</artifactId>
<version>${keymate.sdk.version}</version>
</dependency>

Core only (no Spring, JDK 8+):

pom.xml
<dependency>
<groupId>io.keymate</groupId>
<artifactId>keymate-sdk-core</artifactId>
<version>${keymate.sdk.version}</version>
</dependency>
note

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:

application.yml
keymate:
access-gateway:
base-url: https://gateway.example.com
client-id: demo-spa
timeout: 30000
retry-count: 3
retry-interval: 1000
PropertyRequiredDefaultDescription
base-urlYesBase URL of the Access Gateway
client-idYesKeycloak client identifier
timeoutNo30000Request timeout in milliseconds
retry-countNo3Number of retry attempts on failure
retry-intervalNo1000Interval 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:

Configuration with Builder
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:

Factory 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.

Annotation-based permission check
@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:

Multiple resources
@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:

Context from 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):

PrioritySourceResult
1Method-level mapping path containing a resource or resources segmentFirst non-variable segment after the keyword (e.g., @GetMapping("/resources/users/{id}")users)
2Class-level @RequestMapping pathLast non-variable segment (e.g., @RequestMapping("/api/v1/users")users)
3Controller class nameSuffix (Controller / Resource) stripped and converted to kebab-case (e.g., EmployeeDataControlleremployee-data)

Scope — mapped from the HTTP method annotation: @GetMappingread, @PostMappingcreate, @PutMapping / @PatchMappingupdate, @DeleteMappingdelete.

Context attributes — extracted from @RequestBody fields when no explicit @Context is defined.

Auto-inference in action
@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:

Programmatic permission check
@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.

List all permissions
@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

Programmatic permission listing
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 context
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 ReturnMeaning
trueAll requested resources are granted
falseOne or more resources are denied

Spring exception handling

The Spring module throws specific exceptions that map to HTTP status codes:

ExceptionHTTP StatusCause
UnauthorizedException401Token is invalid, expired, or missing
ForbiddenException403Permission denied or Access Gateway error

ForbiddenException includes error details from the Access Gateway:

Exception handling
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:

ExceptionCause
HttpErrorExceptionHTTP error from the Access Gateway (4xx, 5xx)
RetryExhaustedExceptionAll retry attempts failed
KeymateSdkExceptionGeneral SDK error (parent class)

Error codes

The SDK defines error codes for validation and communication failures:

Error CodeDescription
INVALID_ACCESS_TOKENAccess token is null or blank
INVALID_RESOURCE_NAMEResource name contains invalid characters
INVALID_RESOURCE_SCOPESResource scopes are missing or invalid
INVALID_CLIENT_IDClient ID is null or blank
HTTP_CLIENT_ERRORAccess Gateway returned 4xx
HTTP_SERVER_ERRORAccess Gateway returned 5xx
RETRY_EXHAUSTEDAll retries exhausted
JSON_PARSE_ERRORFailed to parse Access Gateway response

7. Use async operations

Every operation has a CompletableFuture-based async variant:

Async permission check
CompletableFuture<Boolean> future = accessGatewayService.checkPermissionAsync(
accessToken, resources, context
);

future.thenAccept(granted -> {
if (granted) {
// proceed
}
});
Async organization context
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:

DPoP with programmatic usage
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.

warning

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 ForbiddenException and returns 403 Forbidden.
  • If the token is invalid or expired, the SDK throws UnauthorizedException and returns 401 Unauthorized.

How to Verify

  • API evidence: Send a GET /api/users request with a valid access token (Bearer or DPoP). A 200 response confirms the permission check passed.
  • API evidence: Send the same request with an invalid token. A 401 response confirms token validation.
  • API evidence: Send the request with a valid token for a user without the users:list permission. A 403 response confirms enforcement.
  • Logs / traces: Check your application logs for SDK permission check entries. Verify that the Keymate-Decision and Keymate-Decision-Cache response 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

SymptomLikely CauseResolution
Connection timeout to Access Gatewaybase-url is incorrect or the gateway is downVerify the base-url is reachable from your application. Check network policies and firewall rules.
UnauthorizedException on every requestAccess token is expired or invalidConfirm 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_MISMATCHSDK client-id does not match token claimsVerify the client-id in SDK configuration matches the client_id or azp claim in the access token.
DPoP proof validation failsHeader mismatch or clock skewVerify that Keymate-Target-URI and Keymate-Target-Method match the DPoP proof's htu and htm claims.
RetryExhaustedExceptionAccess Gateway is not responding after all retriesCheck gateway health and increase timeout or retry-count if needed.
Spring annotation not interceptingAOP not enabled or module missing from classpathConfirm 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

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.