Px/

Spring Boot Integration

Wire authorization-core into Spring Boot 3 using auto-configuration, AOP interception, and request-scoped header forwarding.

Edit this page on GitHub

The Spring Boot integration is fully auto-configured. Add the dependency and configure application.properties — no @Import, no manual bean setup.

1. Add the dependency#

xml
<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:

rounded-md border px-1.5 py-0.5 font-mono text-[0.82em]
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

The following beans are registered:

  • SpringApplicationConfigProvider — reads config from Spring Environment
  • SpringHeaderExtractor@RequestScope bean extracting all headers from HttpServletRequest
  • SpringResourceCollector — scans DefaultListableBeanFactory for @Resource methods
  • SpringInitialize@PostConstruct runs resource registration on startup
  • ResourceAspect@Around AOP advice intercepting all @Resource-annotated methods
  • ResourceExceptionHandler@ControllerAdvice converting ResponseStatusException to JSON 403
  • SpringEnvironmentUtil — detects dev mode via env.acceptsProfiles(Profiles.of("dev"))

2. Configure application.properties#

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
bash
# Environment variables (not in application.properties)
export ADMIN_CLIENT_ID=iotlab-admin
export ADMIN_CLIENT_SECRET=your-client-secret

Disable enforcement without removing annotations:

properties
ctu.iotlab.resource-config.enabled=false

Disable startup registration in dev profile (detected automatically — no property needed):

properties
spring.profiles.active=dev

3. Annotate your controller methods#

java
@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:

java
@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:

java
@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:

rounded-md border px-1.5 py-0.5 font-mono text-[0.82em]
connection, host, content-length, expect, upgrade, transfer-encoding

This 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:

json
{
  "status":  403,
  "error":   "Forbidden",
  "message": "Resource access denied invoice:read"
}

Customize this by registering your own @ExceptionHandler for ResponseStatusException:

java
@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:

  1. Checks ctu.iotlab.resource-config.enabled — returns early if not "true".
  2. Checks the service name is not ctu-resource-management-service (prevents bootstrap loop).
  3. Calls SpringResourceCollector.collect() which iterates all BeanDefinition entries in DefaultListableBeanFactory, loads each class, and calls ResourceScanner.scan(beanClass).
  4. Calls ResourceInitializer.init(resources) which exchanges a client_credentials token and POSTs all resources to POST /resources/list.
  5. Logs the resource count and duration on completion.

Sample startup output:

rounded-md border px-1.5 py-0.5 font-mono text-[0.82em]
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#

properties
# ── 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
bash
# Required env vars (token exchange credentials)
ADMIN_CLIENT_ID=iotlab-admin
ADMIN_CLIENT_SECRET=super-secret