Security
JWT authentication flow, role-based access control, RolePermissionValidator internals, and the @AllowedRoles annotation.
Authentication Flow
The admin-service acts as an OAuth2 Resource Server. It never issues tokens — it validates JWTs issued by Keycloak.
JWT Claims Processing
Keycloak tokens include roles under the realm_access claim:
{
"realm_access": {
"roles": ["ADMIN", "offline_access", "uma_authorization"]
}
}
The JwtAuthenticationConverter in SecurityConfig maps each role to a Spring GrantedAuthority by prepending ROLE_:
"ADMIN" → "ROLE_ADMIN"
"INTERNAL_SERVICE" → "ROLE_INTERNAL_SERVICE"
Role-to-Operation Matrix
| Operation Key | Required Role | Endpoints |
|---|---|---|
create-client | ROLE_ADMIN | POST /api/admin/inter-service-clients |
query-realm | ROLE_ADMIN | GET/POST /api/admin/realm-roles, GET /api/admin/realm-role |
vault-ops | ROLE_ADMIN | All /api/admin/vault-* endpoints |
| (path-based) | ROLE_INTERNAL_SERVICE | /internal/** |
| ⚠️ (missing) | None | DELETE /api/admin/vault-secrets |
RolePermissionValidator
Bean name: rolePermissionValidator. This is the central enforcement point for operation-level RBAC across all controllers.
@Component
@RequiredArgsConstructor
public class RolePermissionValidator {
private final ApiProperties apiProperties;
public Boolean hasAnyRole(Authentication authentication, String operation) {
List<String> allowedRoles = apiProperties.getApiRoles().get(operation);
if (CommonUtils.isEmpty(allowedRoles)) return false;
Set<String> userRoles = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
boolean hasRole = allowedRoles.stream().anyMatch(userRoles::contains);
if (!hasRole) {
throw new UnAuthorizedException("User does not have valid access for this operation");
}
return true;
}
}
Flow for @PreAuthorize("@rolePermissionValidator.hasAnyRole(authentication, 'vault-ops')"):
- Spring AOP intercepts the controller method call.
- Evaluates the SpEL expression — calls
rolePermissionValidator.hasAnyRole(...). - The validator looks up
"vault-ops"inApiProperties.apiRoles→ resolves to["ROLE_ADMIN"]. - Compares against the JWT-extracted
GrantedAuthorityset. - If no match: throws
UnAuthorizedException(HTTP 403). If match: returnstrueand the call proceeds.
@AllowedRoles Annotation
A meta-annotation that wraps the SpEL expression in a cleaner, declarative form:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@rolePermissionValidator.hasAnyRole(authentication, #allowedRoles.value())")
public @interface AllowedRoles {
String value();
}
Usage (intended):
@AllowedRoles("vault-ops")
@GetMapping("/vault-approle")
public ResponseEntity<?> getAppRoles() { ... }
Adding a New Operation Role
To protect a new endpoint with a custom role mapping, no code changes are required — only config:
Step 1 — Add the operation mapping in application.yaml:
security:
api-roles:
my-new-operation:
- ROLE_ANALYST
- ROLE_ADMIN
Step 2 — Annotate the controller method:
@PreAuthorize("@rolePermissionValidator.hasAnyRole(authentication, 'my-new-operation')")
@GetMapping("/my-endpoint")
public ResponseEntity<?> myEndpoint() { ... }
The validator reads the updated config at runtime — no restart required if using Spring Cloud Config with refresh scope.