Px/

Quarkus Integration

Wire authorization-core into Quarkus 3 using CDI interception, MicroProfile Config, and the @Startup bean for resource registration.

Edit this page on GitHub

The Quarkus integration uses CDI @Interceptor and MicroProfile Config. Add the dependency, configure application.properties, and annotate your JAX-RS endpoints.

1. Add the dependency#

xml
<dependency>
  <groupId>io.gitlab.ctu-iotlab</groupId>
  <artifactId>com.authorization.core</artifactId>
  <version>0.1.5</version>
</dependency>

CDI discovery is enabled by the META-INF/beans.xml shipped inside the jar:

xml
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee" bean-discovery-mode="all"/>

The following CDI beans are registered automatically:

  • QuarkusApplicationConfigProvider@ApplicationScoped, reads config via ConfigProvider (MicroProfile Config)
  • QuarkusHeaderExtractor@RequestScoped, extracts headers from the current JAX-RS ContainerRequestContext
  • QuarkusResourceCollector@ApplicationScoped, scans CDI beans for @Resource methods
  • QuarkusInitialize@Singleton @Startup, runs @PostConstruct for resource registration
  • ResourceInterceptor@Interceptor @Resource, CDI @AroundInvoke interceptor

2. Configure application.properties#

properties
# Required
ctu.iotlab.resource-config.url=https://api.permix.dev
ctu.iotlab.resource-config.service-name=report-service
ctu.iotlab.resource-config.enabled=true

# OIDC server URL (used by TokenProvider to build the token exchange URL)
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/myrealm
bash
# Environment variables (token exchange credentials)
export ADMIN_CLIENT_ID=iotlab-admin
export ADMIN_CLIENT_SECRET=your-client-secret

Disable enforcement in dev mode (Quarkus sets the dev profile automatically with quarkus dev):

properties
# No property needed — dev mode is detected from Quarkus profile automatically.
# To also disable runtime checks in dev:
%dev.ctu.iotlab.resource-config.enabled=false

3. Annotate your JAX-RS resource#

java
@Path("/reports")
@ApplicationScoped
public class ReportResource {

  @Inject
  ReportService reportService;

  @GET
  @Path("/{id}")
  @Produces(MediaType.APPLICATION_JSON)
  @Resource(
    name        = "report:read",
    displayName = "Read Report",
    defaultRoles = {"analyst", "admin"}
  )
  public Response getReport(@PathParam("id") String id) {
    return Response.ok(reportService.findById(id)).build();
  }

  @DELETE
  @Path("/{id}")
  @Resource(
    name        = "report:delete",
    displayName = "Delete Report",
    defaultRoles = {"admin"}
  )
  public Response deleteReport(@PathParam("id") String id) {
    reportService.delete(id);
    return Response.noContent().build();
  }
}

You can also annotate at the class level:

java
@Resource(name = "reports", defaultRoles = {"admin"})
@Path("/reports")
@ApplicationScoped
public class ReportResource { ... }

4. How the CDI interceptor works#

ResourceInterceptor is bound to @Resource via @InterceptorBinding. It runs at Priority.APPLICATION:

java
@Resource
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class ResourceInterceptor {

    @Inject HeaderExtractor headerExtractor;
    @Inject QuarkusApplicationConfigProvider configProvider;

    @AroundInvoke
    public Object intercept(InvocationContext ctx) throws Exception {
        // Check method annotation first, then class annotation
        Resource resource = ctx.getMethod().getAnnotation(Resource.class);
        if (resource == null) {
            resource = ctx.getTarget().getClass().getAnnotation(Resource.class);
        }

        if (resource != null) {
            try {
                ResourceHandler.getInstance(configProvider)
                               .handle(resource, headerExtractor);
            } catch (SecurityException e) {
                throw new ForbiddenException(
                    "Resource access denied: " + resource.name()
                );
            }
        }
        return ctx.proceed();
    }
}

ForbiddenException maps to HTTP 403 Forbidden in Quarkus/RESTEasy.

5. Header forwarding#

QuarkusHeaderExtractor is a @RequestScoped CDI bean that reads all headers from the JAX-RS ContainerRequestContext. Like the Spring integration, headers are sanitized by HeaderSanitizer before being forwarded to the Permix:

Stripped headers: connection, host, content-length, expect, upgrade, transfer-encoding

All other headers — including Authorization (JWT), X-Correlation-ID, and custom tenant headers — are forwarded transparently.

6. Config provider#

QuarkusApplicationConfigProvider uses MicroProfile Config (ConfigProvider.getConfig()). This means all standard MicroProfile Config sources work out of the box:

  • application.properties in src/main/resources
  • Environment variables (e.g. CTU_IOTLAB_RESOURCE_CONFIG_URL)
  • System properties
  • Any custom ConfigSource you register
java
// Reading a config value internally:
ConfigProvider.getConfig()
    .getOptionalValue("ctu.iotlab.resource-config.url", String.class)
    .orElse(null);

7. Startup registration#

QuarkusInitialize is annotated @Singleton @Startup and runs @PostConstruct after the application is fully booted:

  1. Checks ctu.iotlab.resource-config.enabled — exits early if not "true".
  2. Checks service name is not ctu-resource-management-service.
  3. QuarkusResourceCollector.collect() scans all CDI beans for @Resource-annotated methods.
  4. ResourceInitializer.init(resources) performs the client_credentials token exchange and calls POST /resources/list.
  5. Logs resource count and duration (skipped in dev mode).

Dev mode detection: QuarkusEnvironmentUtil.isDevMode() calls ConfigUtils.getProfiles() and checks for "dev". Quarkus quarkus dev sets this automatically — no config needed.

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 5 resources.

8. beans.xml#

The library ships its own META-INF/beans.xml with bean-discovery-mode="all". If your application already has a beans.xml, ensure it does not restrict CDI scanning:

xml
<!-- Good — allows the SDK beans to be discovered -->
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       bean-discovery-mode="all">
</beans>

If your beans.xml uses bean-discovery-mode="annotated", make sure to add @Dependent or another scope annotation on any beans you expect to be discovered.

Full application.properties example#

properties
# ── Authorization SDK ──────────────────────────────────────────────
ctu.iotlab.resource-config.url=https://api.permix.dev
ctu.iotlab.resource-config.service-name=report-service
ctu.iotlab.resource-config.enabled=true

# ── OIDC (Keycloak issuer — used to derive token URL) ─────────────
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/myrealm

# ── OIDC client for inbound token validation ──────────────────────
quarkus.oidc.client-id=report-service
quarkus.oidc.credentials.secret=oidc-secret

# ── Dev overrides (quarkus dev sets %dev profile automatically) ───
%dev.ctu.iotlab.resource-config.enabled=false
bash
# Token exchange credentials (env vars)
ADMIN_CLIENT_ID=iotlab-admin
ADMIN_CLIENT_SECRET=super-secret

Quarkus native image

The SDK uses java.net.http.HttpClient and Jackson for HTTP calls, both of which work in Quarkus native builds. No additional reflection configuration is required for the SDK itself. Ensure your own @Resource-annotated classes are included in the native build.