Spring Boot Integration
Wire authorization-core into Spring Boot 3 using auto-configuration, AOP interception, and request-scoped header forwarding.
Edit this page on GitHubThe Spring Boot integration is fully auto-configured. Add the dependency and configure application.properties — no @Import, no manual bean setup.
1. Add the dependency#
<dependency>
<groupId>io.gitlab.ctu-iotlab</groupId>
<artifactId>com.authorization.core</artifactId>
<version>0.1.5</version>
</dependency>Spring Boot picks up SpringAutoConfiguration automatically via:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsThe following beans are registered:
SpringApplicationConfigProvider— reads config from SpringEnvironmentSpringHeaderExtractor—@RequestScopebean extracting all headers fromHttpServletRequestSpringResourceCollector— scansDefaultListableBeanFactoryfor@ResourcemethodsSpringInitialize—@PostConstructruns resource registration on startupResourceAspect—@AroundAOP advice intercepting all@Resource-annotated methodsResourceExceptionHandler—@ControllerAdviceconvertingResponseStatusExceptionto JSON 403SpringEnvironmentUtil— detects dev mode viaenv.acceptsProfiles(Profiles.of("dev"))
2. Configure application.properties#
# Required
ctu.iotlab.resource-config.url=https://api.permix.dev
ctu.iotlab.resource-config.service-name=invoice-service
ctu.iotlab.resource-config.enabled=true
# OAuth2 client credentials for startup token exchange (Keycloak)
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://keycloak.example.com/realms/myrealm# Environment variables (not in application.properties)
export ADMIN_CLIENT_ID=iotlab-admin
export ADMIN_CLIENT_SECRET=your-client-secretDisable enforcement without removing annotations:
ctu.iotlab.resource-config.enabled=falseDisable startup registration in dev profile (detected automatically — no property needed):
spring.profiles.active=dev3. Annotate your controller methods#
@RestController
@RequestMapping("/invoices")
public class InvoiceController {
@Autowired
private InvoiceService invoiceService;
@GetMapping("/{id}")
@Resource(
name = "invoice:read",
displayName = "Read Invoice",
defaultRoles = {"finance", "admin"}
)
public ResponseEntity<Invoice> getInvoice(@PathVariable String id) {
return ResponseEntity.ok(invoiceService.findById(id));
}
@DeleteMapping("/{id}")
@Resource(
name = "invoice:delete",
displayName = "Delete Invoice",
defaultRoles = {"admin"}
)
public ResponseEntity<Void> deleteInvoice(@PathVariable String id) {
invoiceService.delete(id);
return ResponseEntity.noContent().build();
}
}You can also annotate at the class level to protect all methods:
@Resource(name = "invoices", defaultRoles = {"admin"})
@RestController
@RequestMapping("/invoices")
public class InvoiceController { ... }4. How the AOP aspect works#
ResourceAspect is an @Aspect @Component with a single @Around pointcut:
@Around("@annotation(resource)")
public Object validateMethod(
ProceedingJoinPoint joinPoint,
Resource resource
) throws Throwable {
try {
ResourceHandler.getInstance(configProvider)
.handle(resource, headerExtractor);
} catch (SecurityException e) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
"Resource access denied " + resource.name()
);
}
return joinPoint.proceed();
}ResourceHandler.handle() checks ctu.iotlab.resource-config.enabled. If not "true", it returns immediately without making any HTTP call.
5. Header forwarding#
SpringHeaderExtractor is a @RequestScope bean that reads all headers from the current HttpServletRequest. These headers are forwarded to Permix on every POST /resources/access/check call, after HeaderSanitizer strips the following restricted headers:
connection, host, content-length, expect, upgrade, transfer-encodingThis means the caller's Authorization (JWT), X-Correlation-ID, X-Tenant-ID, and any other custom headers are transparently forwarded to the policy engine.
6. Error response format#
ResourceExceptionHandler (@ControllerAdvice) intercepts ResponseStatusException and returns structured JSON:
{
"status": 403,
"error": "Forbidden",
"message": "Resource access denied invoice:read"
}Customize this by registering your own @ExceptionHandler for ResponseStatusException:
@ControllerAdvice
public class AppExceptionHandler {
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handle(ResponseStatusException ex) {
if (ex.getStatusCode() == HttpStatus.FORBIDDEN) {
return ResponseEntity.status(403).body(Map.of(
"code", "ACCESS_DENIED",
"message", ex.getReason()
));
}
throw ex;
}
}7. Startup registration#
SpringInitialize.init() runs @PostConstruct after the Spring context is fully loaded. It:
- Checks
ctu.iotlab.resource-config.enabled— returns early if not"true". - Checks the service name is not
ctu-resource-management-service(prevents bootstrap loop). - Calls
SpringResourceCollector.collect()which iterates allBeanDefinitionentries inDefaultListableBeanFactory, loads each class, and callsResourceScanner.scan(beanClass). - Calls
ResourceInitializer.init(resources)which exchanges aclient_credentialstoken and POSTs all resources toPOST /resources/list. - Logs the resource count and duration on completion.
Sample startup output:
2025-09-10 08:00:00,123 INFO [com.ctu.iotlab] (main) Resource sync process started. Activated profile prod.
2025-09-10 08:00:00,456 INFO [com.ctu.iotlab] (main) Scanned 8 resources.Dev profile skips registration
When spring.profiles.active=dev, SpringEnvironmentUtil.isDevMode() returns true and the startup scan is skipped with a log message. Runtime enforcement is unaffected — control that separately with ctu.iotlab.resource-config.enabled.
Full application.properties example#
# ── Authorization SDK ──────────────────────────────────────────────
ctu.iotlab.resource-config.url=https://api.permix.dev
ctu.iotlab.resource-config.service-name=invoice-service
ctu.iotlab.resource-config.enabled=true
# ── OIDC (used by TokenProvider to build the token URL) ───────────
spring.security.oauth2.resourceserver.jwt.issuer-uri=\
https://keycloak.example.com/realms/myrealm
# ── Dev profile overrides ─────────────────────────────────────────
%dev.ctu.iotlab.resource-config.enabled=false# Required env vars (token exchange credentials)
ADMIN_CLIENT_ID=iotlab-admin
ADMIN_CLIENT_SECRET=super-secret