User-Managed Access (UMA) 2.0 is an OAuth-based protocol that enables resource owners to control access to their protected resources across multiple domains through a centralized authorization server. For Java applications acting as resource servers—APIs, data stores, content repositories—implementing UMA 2.0 protection provides fine-grained, user-managed access control that extends beyond traditional OAuth scopes.
What is UMA 2.0?
UMA 2.0 is a federated authorization protocol that builds on OAuth 2.0 to enable resource owners (users) to manage access to their resources across different domains and applications. Key concepts include:
- Resource Owner: The user who owns the protected data
- Resource Server (RS): The API or service hosting the protected resources (your Java application)
- Authorization Server (AS): Central server managing resource owner policies and permissions
- Client: Application seeking access to resources
- Requesting Party (RqP): The entity (person or application) trying to access resources
Why UMA 2.0 is Critical for Java Resource Servers
- User-Centric Authorization: Resource owners control access policies, not just application developers.
- Cross-Domain Federation: Access policies work across multiple resource servers and domains.
- Fine-Grained Permissions: Beyond OAuth scopes, UMA supports resource-specific permissions.
- Claim-Based Access: Access can be based on user attributes and claims.
- Audit and Transparency: Complete visibility into who accessed what and when.
UMA 2.0 Protocol Flow
1. Client requests resource from Resource Server (RS) 2. RS returns 401 with permission ticket and AS location 3. Client requests access token from Authorization Server (AS) with ticket 4. AS checks resource owner policies, may request claims 5. AS issues RPT (Requesting Party Token) to client 6. Client retries request with RPT 7. RS validates RPT and returns resource
Implementing UMA 2.0 Protection in Java
1. Maven Dependencies
<dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2 Resource Server --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <!-- JWT Support --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37.2</version> </dependency> <!-- For UMA-specific features --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> </dependency> </dependencies>
2. UMA Configuration Properties
# application-uma.yml
uma:
enabled: true
authorization-server:
issuer: https://auth.example.com
pat-endpoint: ${uma.authorization-server.issuer}/uma/pat
rpt-endpoint: ${uma.authorization-server.issuer}/uma/rpt
permission-endpoint: ${uma.authorization-server.issuer}/uma/permission
resource-registration-endpoint: ${uma.authorization-server.issuer}/uma/resource
introspection-endpoint: ${uma.authorization-server.issuer}/uma/introspect
jwks-uri: ${uma.authorization-server.issuer}/uma/jwks
resource-server:
id: rs-healthcare-api
name: Healthcare API Resource Server
pat-client-id: rs-healthcare-client
pat-client-secret: ${RS_PAT_CLIENT_SECRET}
resources:
- id: patient-records
name: Patient Medical Records
type: http://healthcare.example/rs/patient-record
icon-uri: https://example.com/icons/medical.png
scopes:
- view
- update
- share
- delete
- id: appointments
name: Patient Appointments
type: http://healthcare.example/rs/appointment
scopes:
- view
- create
- cancel
3. UMA Protection Service
@Component
public class UMAProtectionService {
private final RestTemplate restTemplate;
private final UMAConfiguration umaConfig;
private final ResourceRepository resourceRepository;
private String patToken; // Protection API Token (cached)
private Instant patExpiry;
public UMAProtectionService(
@Qualifier("umaRestTemplate") RestTemplate restTemplate,
UMAConfiguration umaConfig,
ResourceRepository resourceRepository) {
this.restTemplate = restTemplate;
this.umaConfig = umaConfig;
this.resourceRepository = resourceRepository;
}
/**
* Register a resource with the UMA Authorization Server
*/
public ResourceRegistration registerResource(ResourceDescription resource) {
ensurePAT();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(patToken);
// Build UMA resource description
Map<String, Object> resourceDesc = new HashMap<>();
resourceDesc.put("name", resource.getName());
resourceDesc.put("type", resource.getType());
resourceDesc.put("icon_uri", resource.getIconUri());
resourceDesc.put("resource_scopes", resource.getScopes());
HttpEntity<Map<String, Object>> entity =
new HttpEntity<>(resourceDesc, headers);
ResponseEntity<ResourceRegistrationResponse> response = restTemplate.exchange(
umaConfig.getResourceRegistrationEndpoint(),
HttpMethod.POST,
entity,
ResourceRegistrationResponse.class
);
ResourceRegistration registration = response.getBody();
// Store mapping between local resource ID and UMA resource ID
resourceRepository.storeResourceMapping(
resource.getLocalId(),
registration.getResourceId()
);
return registration;
}
/**
* Update an existing resource
*/
public void updateResource(String resourceId, ResourceDescription updates) {
ensurePAT();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(patToken);
HttpEntity<ResourceDescription> entity = new HttpEntity<>(updates, headers);
restTemplate.exchange(
umaConfig.getResourceRegistrationEndpoint() + "/" + resourceId,
HttpMethod.PUT,
entity,
Void.class
);
}
/**
* Delete a resource
*/
public void deleteResource(String resourceId) {
ensurePAT();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(patToken);
HttpEntity<?> entity = new HttpEntity<>(headers);
restTemplate.exchange(
umaConfig.getResourceRegistrationEndpoint() + "/" + resourceId,
HttpMethod.DELETE,
entity,
Void.class
);
}
/**
* Create a permission ticket for a resource
*/
public PermissionTicket createPermissionTicket(
String resourceId,
List<String> scopes) {
ensurePAT();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(patToken);
List<Map<String, Object>> permissions = List.of(
Map.of(
"resource_id", resourceId,
"resource_scopes", scopes
)
);
HttpEntity<List<Map<String, Object>>> entity =
new HttpEntity<>(permissions, headers);
ResponseEntity<PermissionTicketResponse> response = restTemplate.exchange(
umaConfig.getPermissionEndpoint(),
HttpMethod.POST,
entity,
PermissionTicketResponse.class
);
return response.getBody().getTicket();
}
/**
* Validate an RPT (Requesting Party Token)
*/
public RPTIntrospection introspectRPT(String rpt) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("token", rpt);
body.add("token_type_hint", "requesting_party_token");
HttpEntity<MultiValueMap<String, String>> entity =
new HttpEntity<>(body, headers);
ResponseEntity<RPTIntrospection> response = restTemplate.exchange(
umaConfig.getIntrospectionEndpoint(),
HttpMethod.POST,
entity,
RPTIntrospection.class
);
return response.getBody();
}
/**
* Ensure we have a valid PAT (Protection API Token)
*/
private synchronized void ensurePAT() {
if (patToken != null && patExpiry != null &&
patExpiry.isAfter(Instant.now().plusSeconds(60))) {
return;
}
// Request new PAT using client credentials
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
body.add("scope", "uma_protection");
body.add("client_id", umaConfig.getResourceServer().getPatClientId());
body.add("client_secret", umaConfig.getResourceServer().getPatClientSecret());
HttpEntity<MultiValueMap<String, String>> entity =
new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.exchange(
umaConfig.getPatEndpoint(),
HttpMethod.POST,
entity,
Map.class
);
Map<String, Object> tokenResponse = response.getBody();
this.patToken = (String) tokenResponse.get("access_token");
long expiresIn = ((Number) tokenResponse.get("expires_in")).longValue();
this.patExpiry = Instant.now().plusSeconds(expiresIn);
}
// Model classes
@Data
@Builder
public static class ResourceDescription {
private String localId;
private String name;
private String type;
private String iconUri;
private List<String> scopes;
}
@Data
public static class ResourceRegistrationResponse {
private String resourceId;
private String userAccessPolicyUri;
private Instant createdAt;
}
@Data
public static class PermissionTicketResponse {
private PermissionTicket ticket;
private Instant createdAt;
}
@Data
public static class PermissionTicket {
private String ticket;
private Instant expiresAt;
}
@Data
public static class RPTIntrospection {
private boolean active;
private String sub;
private String clientId;
private Instant exp;
private Instant iat;
private String issuer;
private String jti;
private String aud;
private List<Permission> permissions;
@Data
public static class Permission {
private String resourceId;
private List<String> resourceScopes;
private Instant expiresAt;
}
}
}
4. UMA-Aware Resource Server Filter
@Component
public class UMARequestFilter implements Filter {
private final UMAProtectionService umaProtectionService;
private final ResourcePermissionResolver permissionResolver;
private final UMAConfiguration umaConfig;
public UMARequestFilter(
UMAProtectionService umaProtectionService,
ResourcePermissionResolver permissionResolver,
UMAConfiguration umaConfig) {
this.umaProtectionService = umaProtectionService;
this.permissionResolver = permissionResolver;
this.umaConfig = umaConfig;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Check if this is an UMA-protected resource
String path = httpRequest.getRequestURI();
Optional<ProtectedResource> protectedResource =
permissionResolver.findProtectedResource(path);
if (protectedResource.isEmpty()) {
// Not protected, continue
chain.doFilter(request, response);
return;
}
// Check for RPT in Authorization header
String authHeader = httpRequest.getHeader("Authorization");
String rpt = extractRPT(authHeader);
if (rpt == null) {
// No RPT, return UMA challenge
sendUMAChallenge(httpResponse, protectedResource.get());
return;
}
// Introspect RPT
UMAProtectionService.RPTIntrospection introspection =
umaProtectionService.introspectRPT(rpt);
if (!introspection.isActive()) {
// RPT invalid or expired
sendUMAChallenge(httpResponse, protectedResource.get());
return;
}
// Check if RPT grants access to this resource
boolean hasPermission = hasRequiredPermission(
introspection,
protectedResource.get(),
httpRequest.getMethod()
);
if (!hasPermission) {
// Valid RPT but insufficient permissions
httpResponse.setStatus(HttpStatus.FORBIDDEN.value());
httpResponse.getWriter().write("Insufficient permissions");
return;
}
// Add UMA context to request for downstream use
httpRequest.setAttribute("uma_introspection", introspection);
httpRequest.setAttribute("uma_resource", protectedResource.get());
chain.doFilter(request, response);
}
private String extractRPT(String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private void sendUMAChallenge(
HttpServletResponse response,
ProtectedResource resource) throws IOException {
// Create permission ticket for this resource
PermissionTicket ticket = umaProtectionService.createPermissionTicket(
resource.getUmaResourceId(),
resource.getRequiredScopesForMethod(request.getMethod())
);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setHeader("WWW-Authenticate",
String.format("UMA realm=\"%s\", ticket=\"%s\"",
umaConfig.getAuthorizationServer().getIssuer(),
ticket.getTicket()
)
);
// Include AS location
response.setHeader("AS-Discovery",
umaConfig.getAuthorizationServer().getIssuer() + "/.well-known/uma-configuration");
}
private boolean hasRequiredPermission(
UMAProtectionService.RPTIntrospection introspection,
ProtectedResource resource,
String method) {
List<String> requiredScopes = resource.getRequiredScopesForMethod(method);
return introspection.getPermissions().stream()
.filter(p -> p.getResourceId().equals(resource.getUmaResourceId()))
.anyMatch(p -> p.getResourceScopes().containsAll(requiredScopes));
}
@Data
public static class ProtectedResource {
private String path;
private String umaResourceId;
private Map<String, List<String>> methodScopes;
public List<String> getRequiredScopesForMethod(String method) {
return methodScopes.getOrDefault(method, List.of());
}
}
}
5. UMA Resource Registration on Startup
@Component
public class UMAResourceInitializer implements ApplicationListener<ApplicationReadyEvent> {
private final UMAProtectionService umaProtectionService;
private final ResourceRepository resourceRepository;
private final UMAConfiguration umaConfig;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// Register all configured resources with UMA Authorization Server
for (UMAConfiguration.ResourceConfig resource : umaConfig.getResources()) {
try {
// Check if already registered
Optional<String> existingId = resourceRepository.getUmaResourceId(
resource.getId());
if (existingId.isPresent()) {
// Update existing resource
updateResource(existingId.get(), resource);
} else {
// Register new resource
registerResource(resource);
}
} catch (Exception e) {
log.error("Failed to register UMA resource: {}", resource.getId(), e);
}
}
}
private void registerResource(UMAConfiguration.ResourceConfig resource) {
UMAProtectionService.ResourceDescription description =
UMAProtectionService.ResourceDescription.builder()
.localId(resource.getId())
.name(resource.getName())
.type(resource.getType())
.iconUri(resource.getIconUri())
.scopes(resource.getScopes())
.build();
UMAProtectionService.ResourceRegistration registration =
umaProtectionService.registerResource(description);
resourceRepository.storeResourceMapping(
resource.getId(),
registration.getResourceId()
);
log.info("Registered UMA resource: {} with ID: {}",
resource.getId(), registration.getResourceId());
}
private void updateResource(String umaResourceId,
UMAConfiguration.ResourceConfig resource) {
UMAProtectionService.ResourceDescription description =
UMAProtectionService.ResourceDescription.builder()
.name(resource.getName())
.type(resource.getType())
.iconUri(resource.getIconUri())
.scopes(resource.getScopes())
.build();
umaProtectionService.updateResource(umaResourceId, description);
log.info("Updated UMA resource: {}", resource.getId());
}
}
6. UMA-Protected REST Controller
@RestController
@RequestMapping("/api/health")
public class HealthRecordController {
private final HealthRecordService recordService;
@GetMapping("/records/{patientId}")
public ResponseEntity<HealthRecord> getPatientRecord(
@PathVariable String patientId,
HttpServletRequest request) {
// Retrieve UMA context from request
UMAProtectionService.RPTIntrospection introspection =
(UMAProtectionService.RPTIntrospection)
request.getAttribute("uma_introspection");
// Extract requesting party info
String requestingParty = introspection.getSub();
List<String> permissions = introspection.getPermissions().stream()
.flatMap(p -> p.getResourceScopes().stream())
.collect(Collectors.toList());
// Log access for audit
auditService.logAccess(
patientId,
requestingParty,
permissions,
"GET"
);
// Business logic
HealthRecord record = recordService.findByPatientId(patientId);
return ResponseEntity.ok(record);
}
@PostMapping("/records/{patientId}")
@PreAuthorize("hasUmaScope('update')")
public ResponseEntity<HealthRecord> updatePatientRecord(
@PathVariable String patientId,
@RequestBody HealthRecordUpdate update,
HttpServletRequest request) {
// @PreAuthorize with custom UMA scope handler
HealthRecord updated = recordService.update(patientId, update);
return ResponseEntity.ok(updated);
}
@PostMapping("/records/{patientId}/share")
@PreAuthorize("hasUmaScope('share')")
public ResponseEntity<ShareResponse> shareRecord(
@PathVariable String patientId,
@RequestBody ShareRequest shareRequest,
HttpServletRequest request) {
// Create permission ticket for sharing
PermissionTicket ticket = umaProtectionService.createPermissionTicket(
getResourceIdForPatient(patientId),
List.of("view")
);
// Generate sharing link
String shareLink = sharingService.createShareLink(
patientId,
shareRequest.getExpiryDays(),
ticket
);
return ResponseEntity.ok(new ShareResponse(shareLink));
}
}
7. Custom UMA Security Expression Handler
@Component
public class UMASecurityExpressionRoot extends SecurityExpressionRoot {
private UMAProtectionService.RPTIntrospection introspection;
public UMASecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
public void setIntrospection(UMAProtectionService.RPTIntrospection introspection) {
this.introspection = introspection;
}
public boolean hasUmaScope(String scope) {
if (introspection == null) {
return false;
}
return introspection.getPermissions().stream()
.flatMap(p -> p.getResourceScopes().stream())
.anyMatch(s -> s.equals(scope));
}
public boolean hasUmaScopeForResource(String resourceId, String scope) {
if (introspection == null) {
return false;
}
return introspection.getPermissions().stream()
.filter(p -> p.getResourceId().equals(resourceId))
.flatMap(p -> p.getResourceScopes().stream())
.anyMatch(s -> s.equals(scope));
}
public String getRequestingParty() {
return introspection != null ? introspection.getSub() : null;
}
}
@Component
public class UMASecurityExpressionHandler implements
MethodSecurityExpressionHandler {
private final AuthenticationTrustResolver trustResolver =
new AuthenticationTrustResolverImpl();
@Override
public EvaluationContext createEvaluationContext(
Authentication authentication,
MethodInvocation invocation) {
SecurityExpressionRoot root = new UMASecurityExpressionRoot(authentication);
root.setTrustResolver(trustResolver);
// Extract UMA context from invocation
if (invocation instanceof ReflectiveMethodInvocation) {
Object[] args = invocation.getArguments();
for (Object arg : args) {
if (arg instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) arg;
Object introspection = request.getAttribute("uma_introspection");
if (introspection instanceof UMAProtectionService.RPTIntrospection) {
((UMASecurityExpressionRoot) root).setIntrospection(
(UMAProtectionService.RPTIntrospection) introspection);
}
}
}
}
return new StandardEvaluationContext(root);
}
}
8. UMA Configuration Endpoint (for discovery)
@RestController
@RequestMapping("/.well-known")
public class UMAConfigurationEndpoint {
private final UMAConfiguration umaConfig;
@GetMapping("/uma-configuration")
public ResponseEntity<UMAConfigurationResponse> getUMAConfiguration() {
UMAConfigurationResponse config = UMAConfigurationResponse.builder()
.issuer(umaConfig.getAuthorizationServer().getIssuer())
.tokenEndpoint(umaConfig.getAuthorizationServer().getTokenEndpoint())
.authorizationEndpoint(umaConfig.getAuthorizationServer().getAuthEndpoint())
.patEndpoint(umaConfig.getAuthorizationServer().getPatEndpoint())
.rptEndpoint(umaConfig.getAuthorizationServer().getRptEndpoint())
.permissionEndpoint(umaConfig.getAuthorizationServer().getPermissionEndpoint())
.resourceRegistrationEndpoint(
umaConfig.getAuthorizationServer().getResourceRegistrationEndpoint())
.introspectionEndpoint(umaConfig.getAuthorizationServer().getIntrospectionEndpoint())
.jwksUri(umaConfig.getAuthorizationServer().getJwksUri())
.build();
return ResponseEntity.ok(config);
}
@Data
@Builder
public static class UMAConfigurationResponse {
private String issuer;
private String tokenEndpoint;
private String authorizationEndpoint;
private String patEndpoint;
private String rptEndpoint;
private String permissionEndpoint;
private String resourceRegistrationEndpoint;
private String introspectionEndpoint;
private String jwksUri;
private List<String> scopesSupported = Arrays.asList(
"openid", "profile", "uma_protection", "uma_authorization");
private List<String> responseTypesSupported =
Arrays.asList("token", "code");
private List<String> grantTypesSupported =
Arrays.asList("authorization_code", "implicit", "refresh_token",
"client_credentials", "urn:ietf:params:oauth:grant-type:uma-ticket");
private List<String> tokenEndpointAuthMethodsSupported =
Arrays.asList("client_secret_basic", "client_secret_post");
}
}
9. UMA Claim Collection and Resolution
@Service
public class UMAClaimsService {
private final UMAProtectionService umaProtectionService;
private final ClaimsRepository claimsRepository;
/**
* Handle claim collection when additional claims are needed
*/
public ClaimCollectionResponse requestClaims(
String ticket,
List<String> neededClaims) {
// Generate claims redirect URI
String claimsRedirectUri = generateClaimsRedirectUri(ticket);
// Store pending claims request
claimsRepository.storePendingClaims(ticket, neededClaims);
return ClaimCollectionResponse.builder()
.claimsRedirectUri(claimsRedirectUri)
.ticket(ticket)
.neededClaims(neededClaims)
.build();
}
/**
* Submit claims for a ticket
*/
public boolean submitClaims(String ticket, Map<String, Object> claims) {
// Validate submitted claims
List<String> neededClaims = claimsRepository.getPendingClaims(ticket);
if (!claims.keySet().containsAll(neededClaims)) {
return false;
}
// Store claims for this ticket
claimsRepository.storeClaims(ticket, claims);
return true;
}
/**
* Get RPT after claims submission
*/
public String requestRPTWithClaims(String ticket) {
// Retrieve claims for this ticket
Map<String, Object> claims = claimsRepository.getClaims(ticket);
// Request RPT with claims
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> request = new HashMap<>();
request.put("ticket", ticket);
request.put("claim_token", encodeClaims(claims));
request.put("claim_token_format",
"http://openid.net/specs/openid-connect-core-1_0.html#IDToken");
HttpEntity<Map<String, Object>> entity =
new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.exchange(
umaConfig.getRptEndpoint(),
HttpMethod.POST,
entity,
Map.class
);
return (String) response.getBody().get("access_token");
}
@Data
@Builder
public static class ClaimCollectionResponse {
private String claimsRedirectUri;
private String ticket;
private List<String> neededClaims;
}
}
10. UMA Audit and Logging
@Component
public class UMAAuditLogger {
private final AuditRepository auditRepository;
@EventListener
public void handleUMAAccess(UMAAccessEvent event) {
AuditEntry entry = AuditEntry.builder()
.timestamp(Instant.now())
.resourceId(event.getResourceId())
.requestingParty(event.getRequestingParty())
.clientId(event.getClientId())
.action(event.getAction())
.scopes(event.getScopes())
.ticket(event.getTicket())
.rpt(event.getRpt())
.outcome(event.getOutcome())
.build();
auditRepository.save(entry);
// Log for compliance
log.info("UMA Access: resource={}, rqp={}, scopes={}, outcome={}",
event.getResourceId(),
event.getRequestingParty(),
event.getScopes(),
event.getOutcome()
);
}
public List<AuditEntry> getAccessHistory(String resourceId, Instant from, Instant to) {
return auditRepository.findByResourceIdAndTimestampBetween(
resourceId, from, to);
}
@Data
@Builder
public static class AuditEntry {
private Instant timestamp;
private String resourceId;
private String requestingParty;
private String clientId;
private String action;
private List<String> scopes;
private String ticket;
private String rpt;
private String outcome;
}
@Data
public static class UMAAccessEvent {
private String resourceId;
private String requestingParty;
private String clientId;
private String action;
private List<String> scopes;
private String ticket;
private String rpt;
private String outcome;
}
}
Testing UMA Protection
@SpringBootTest
@AutoConfigureMockMvc
public class UMAProtectionTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UMAProtectionService umaProtectionService;
@Test
void testProtectedResource_WithoutRPT_ReturnsUMAChallenge() throws Exception {
// Given a protected resource
String resourcePath = "/api/health/records/patient-123";
// When requesting without RPT
mockMvc.perform(get(resourcePath))
.andExpect(status().isUnauthorized())
.andExpect(header().string("WWW-Authenticate",
containsString("UMA realm=")))
.andExpect(header().string("WWW-Authenticate",
containsString("ticket=")))
.andExpect(header().string("AS-Discovery",
containsString(".well-known/uma-configuration")));
}
@Test
void testProtectedResource_WithValidRPT_GrantsAccess() throws Exception {
// Given a valid RPT
String rpt = "valid-rpt-token";
String resourcePath = "/api/health/records/patient-123";
// Mock RPT introspection
UMAProtectionService.RPTIntrospection introspection =
new UMAProtectionService.RPTIntrospection();
introspection.setActive(true);
introspection.setPermissions(List.of(
createPermission("patient-123-resource", List.of("view"))
));
when(umaProtectionService.introspectRPT(anyString()))
.thenReturn(introspection);
// When requesting with RPT
mockMvc.perform(get(resourcePath)
.header("Authorization", "Bearer " + rpt))
.andExpect(status().isOk());
}
@Test
void testProtectedResource_WithInvalidRPT_ReturnsForbidden() throws Exception {
// Given an RPT without required scopes
String rpt = "insufficient-rpt";
String resourcePath = "/api/health/records/patient-123";
// Mock RPT introspection with insufficient permissions
UMAProtectionService.RPTIntrospection introspection =
new UMAProtectionService.RPTIntrospection();
introspection.setActive(true);
introspection.setPermissions(List.of(
createPermission("different-resource", List.of("view"))
));
when(umaProtectionService.introspectRPT(anyString()))
.thenReturn(introspection);
// When requesting with RPT
mockMvc.perform(get(resourcePath)
.header("Authorization", "Bearer " + rpt))
.andExpect(status().isForbidden());
}
}
Best Practices for UMA Protection
- Register All Resources: Ensure all protected resources are properly registered with the authorization server.
- Cache PAT Tokens: Protection API Tokens (PAT) should be cached and refreshed proactively.
- Validate RPTs Thoroughly: Check not just active status but also specific resource permissions.
- Implement Audit Logging: Track all UMA access for compliance and security analysis.
- Handle Claim Collection Gracefully: Provide clear user interfaces for claim collection when needed.
- Use Short-Lived Tickets: Permission tickets should have short expiration times.
- Secure PAT Storage: Protection API Tokens have broad permissions; store them securely.
Conclusion
UMA 2.0 provides a powerful framework for implementing user-managed access control in Java resource servers. By separating resource protection from policy management, UMA enables fine-grained, federated authorization that puts resource owners in control.
For Java applications exposing sensitive APIs—healthcare records, financial data, personal information—UMA 2.0 offers a standards-based approach to delegated authorization that scales across domains and applications. With proper implementation of resource registration, permission tickets, RPT validation, and claim collection, Java resource servers can provide robust, user-centric access control that meets the demands of modern, privacy-conscious applications.