Security Overview and Architecture

This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against.

Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change.

This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Quarkus Security is one coherent model shared by every authentication mechanism and every extension that adds one — HTTP Basic, form-based, mutual TLS, quarkus-oidc, quarkus-smallrye-jwt, quarkus-security-jdbc, quarkus-security-jpa, WebAuthn, and any custom HttpAuthenticationMechanism all end up producing the same SecurityIdentity type, checked by the same @RolesAllowed/@Authenticated/@PermissionsAllowed annotations and the same quarkus.http.auth.permission.* configuration. This page covers that shared core: how a request is authenticated, how the resulting identity is authorized, and the customisation points and testing support that apply regardless of which concrete mechanism is in play. The mechanisms themselves — Basic/form/JDBC/JPA/LDAP/WebAuthn/mTLS and JWT/OIDC/Keycloak — are covered on their own pages; protocol-level OAuth 2.0/OIDC/JWT theory itself lives in the sibling OAuth Reference and is only linked from here, never repeated.

Authentication: SecurityIdentity and identity providers

The SecurityIdentity

io.quarkus.security.identity.SecurityIdentity is the single, immutable representation of "who is making this request" that every part of Quarkus Security reads from — its Principal, granted roles, arbitrary attributes, and the raw credential that produced it:

package com.example.security;

import io.quarkus.security.identity.SecurityIdentity;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@ApplicationScoped
@Path("/whoami")
public class WhoAmIResource {

    @Inject
    SecurityIdentity identity;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String whoAmI() {
        if (identity.isAnonymous()) {
            return "anonymous";
        }
        return identity.getPrincipal().getName() + " roles=" + identity.getRoles();
    }
}

An unauthenticated request is not an error by itself — it resolves to the built-in anonymous SecurityIdentity (isAnonymous() returns true, no roles granted), which still flows through authorization; it is simply denied by any check other than @PermitAll.

IdentityProvider and the IdentityProviderManager

An io.quarkus.security.identity.IdentityProvider<T extends AuthenticationRequest> turns one specific kind of credential (a username/password pair, a bearer token, a client certificate) into a SecurityIdentity. Each authentication mechanism produces an AuthenticationRequest subtype and hands it to the io.quarkus.security.identity.IdentityProviderManager, which looks up the IdentityProvider registered for that exact request type and delegates to it — so a project can add quarkus-security-jdbc (a UsernamePasswordAuthenticationRequest provider) and quarkus-smallrye-jwt (a TokenAuthenticationRequest provider) side by side, each handling its own credential kind without knowing the other exists.

Authentication mechanisms and how one is selected

An io.quarkus.security.spi.runtime.HttpAuthenticationMechanism inspects an incoming request for credentials of its own kind — an Authorization: Basic header, a session cookie, a client certificate from the TLS handshake — and, if found, builds the matching AuthenticationRequest. Several extensions can register a mechanism at once (a project might have quarkus-elytron-security-properties-file for Basic auth on one path and quarkus-oidc for bearer tokens on another); Quarkus dispatches to the right one per request using each mechanism’s own applicability test plus, when more than one could apply, quarkus.http.auth.proactive and per-path HttpSecurityPolicy selection (see Path-based policies and path matching).

Proactive authentication and why you might turn it off

By default (quarkus.http.auth.proactive=true), Quarkus authenticates every request — running every registered mechanism against it — before routing decides which resource method will handle it, so SecurityIdentity is already populated by the time any @RolesAllowed check or application code runs. Turning it off:

quarkus.http.auth.proactive=false

defers authentication until something actually asks for the SecurityIdentity — an annotation-based check on the matched endpoint, or an explicit injection point. This matters in two situations: a route that must stay genuinely public even when a client sends credentials that happen to be invalid (proactive auth would reject the request outright before the route’s own @PermitAll gets a chance to run), and a mechanism whose credential extraction is expensive or has side effects that should only run for endpoints that actually require authentication. The trade-off is that identity-dependent logic in a @PermitAll or unsecured endpoint can no longer assume SecurityIdentity has already been resolved — it has to request it explicitly (blocking code can call identity.checkPermissionBlocking(…​) or read it once the reactive Uni<SecurityIdentity> completes).

Authorization

Annotation-based checks

Annotation Effect

@RolesAllowed({"admin", "editor"})

Allows the call only if the current SecurityIdentity has been granted at least one of the listed roles. jakarta.annotation.security.RolesAllowed, the standard Jakarta Security annotation.

@Authenticated

Allows the call for any non-anonymous identity, regardless of role — shorthand for @RolesAllowed("**"). io.quarkus.security.Authenticated.

@PermitAll

Allows the call unconditionally, including for the anonymous identity. jakarta.annotation.security.PermitAll.

@DenyAll

Rejects the call unconditionally, even for an authenticated identity with every role — useful for disabling an endpoint without deleting it. jakarta.annotation.security.DenyAll.

package com.example.book;

import io.quarkus.security.Authenticated;

import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@Path("/books")
public class BookResource {

    @GET
    @PermitAll
    public String list() {
        return "public catalog";
    }

    @POST
    @RolesAllowed("editor")
    public String create() {
        return "created";
    }

    @GET
    @Path("/mine")
    @Authenticated
    public String mine() {
        return "your books";
    }

    @GET
    @Path("/legacy")
    @DenyAll
    public String legacy() {
        return "unreachable";
    }
}

These annotations work on a CDI bean method (including a Quarkus REST resource method), on the class (applying to every method that does not declare its own), or on a plain @ApplicationScoped service method reached from elsewhere in the call graph — Quarkus Security is not tied to HTTP resources specifically.

Path-based policies and path matching

quarkus.http.auth.permission. declares authorization *before a request ever reaches a resource method or even CDI dispatch, matched purely on the request path and HTTP method:

quarkus.http.auth.permission.public.paths=/books,/health/*
quarkus.http.auth.permission.public.policy=permit

quarkus.http.auth.permission.admin.paths=/admin/*
quarkus.http.auth.permission.admin.policy=role-policy
quarkus.http.auth.permission.admin.roles-allowed=admin

quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated

Each named permission group under quarkus.http.auth.permission."<name>" lists paths (comma-separated, * suffix for a prefix match) and a policy — one of the built-in permit/deny/authenticated policies, or role-policy plus roles-allowed for a role check. Quarkus’s PathMatchingHttpSecurityPolicy evaluates every matching group for a request and combines them (the most specific path match wins for a given policy type); this happens ahead of, and independently of, any annotation on the eventual resource method.

Method vs. endpoint security

Annotation-based checks and path-based policies are complementary, not redundant: a path policy gives a single place to see "everything under /admin/* requires the admin role" without hunting through every resource class, and still runs even for a request that never reaches CDI dispatch (a static resource, a non-CDI Vert.x route); an annotation gives per-method precision and travels with the code it protects, including for methods reached through means other than an inbound HTTP request. A path policy that denies a request rejects it before an annotation on the matched method is ever evaluated — a path-level deny cannot be overridden by a more permissive method-level annotation.

A custom io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy bean can implement authorization logic that neither declarative form expresses — time-of-day restrictions, IP allow-listing, a check against a remote policy service — registered as a CDI bean and consulted the same way the built-in path policies are.

SecurityIdentity augmentation

A io.quarkus.security.identity.SecurityIdentityAugmentor CDI bean runs immediately after an IdentityProvider builds a SecurityIdentity, and can add roles or attributes derived from data the identity provider itself does not know about — a role looked up from a separate authorization service, or a tenant identifier resolved from the request:

package com.example.security;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class TenantRoleAugmentor implements SecurityIdentityAugmentor {

    @Override
    public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
        if (identity.isAnonymous()) {
            return Uni.createFrom().item(identity);
        }
        QuarkusSecurityIdentity augmented = QuarkusSecurityIdentity.builder(identity)
                .addRole("tenant-user")
                .build();
        return Uni.createFrom().item(augmented);
    }
}

Because SecurityIdentity is immutable, augmentation always builds and returns a new instance (QuarkusSecurityIdentity.builder(identity) copies the original and layers additions on top) rather than mutating the one it received.

Custom permission checks with @PermissionsAllowed

io.quarkus.security.PermissionsAllowed checks a fine-grained, application-defined java.security.Permission rather than a fixed role name — the right tool when "can this user edit this specific book" depends on data (ownership, tenant) that a role alone cannot express:

package com.example.book;

import java.security.Permission;

import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@ApplicationScoped
@Path("/books")
public class BookEditResource {

    @Inject
    SecurityIdentity identity;

    @Inject
    BookRepository bookRepository;

    @PUT
    @Path("/{id}")
    @PermissionsAllowed("book:update")
    public void update(@PathParam("id") Long id, String newTitle) {
        bookRepository.rename(id, newTitle);
    }
}

class BookOwnershipPermission extends Permission {

    BookOwnershipPermission(String name) {
        super(name);
    }

    @Override
    public boolean implies(Permission permission) {
        return permission instanceof BookOwnershipPermission
                && getName().equals(permission.getName());
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof BookOwnershipPermission
                && getName().equals(((BookOwnershipPermission) other).getName());
    }

    @Override
    public int hashCode() {
        return getName().hashCode();
    }
}

The permission name given to @PermissionsAllowed ("book:update" above) is resolved against a io.quarkus.security.spi.runtime.SecurityCheck supplied by an application-registered io.quarkus.security.identity.IdentityProvider-independent mechanism — in practice, most projects instead implement the check directly against SecurityIdentity.checkPermission(Permission)/checkPermissionBlocking inside the method body, or bind method parameters into the permission via @PermissionsAllowed(value = "book:update", params = "id"), which is the more common pattern for ownership-style checks that need request data the permission name alone cannot carry.

CORS and CSRF prevention

CORS

Cross-Origin Resource Sharing is handled centrally by the HTTP layer, not per-resource — enable and configure it under quarkus.http.cors:

quarkus.http.cors=true
quarkus.http.cors.origins=https://app.example.com
quarkus.http.cors.methods=GET,POST,PUT,DELETE
quarkus.http.cors.headers=Content-Type,Authorization
quarkus.http.cors.access-control-max-age=24H

Leaving quarkus.http.cors.origins unset while quarkus.http.cors=true reflects every request’s own Origin header back as allowed, which is convenient for local development but should never reach a production configuration — an explicit origin allow-list is the safe default.

CSRF prevention

quarkus-csrf-reactive protects a session-cookie-authenticated, browser-facing endpoint against Cross-Site Request Forgery by requiring a matching double-submit token on state-changing requests:

./mvnw quarkus:add-extension -Dextensions=csrf-reactive
quarkus.csrf-reactive.cookie-name=csrf-token
quarkus.csrf-reactive.token-header-name=X-CSRF-TOKEN
quarkus.csrf-reactive.create-token-path=/books

The extension issues a random token as a cookie on a configured GET path; the client must echo that same value back in the configured header (or a hidden form field) on the following POST/PUT/DELETE, and the request is rejected if the two do not match. This only matters for authentication that rides in a cookie the browser sends automatically (session cookies, quarkus-oidc web-app mode); a bearer-token API consumed by a non-browser client is not vulnerable to CSRF in the same way, since nothing forces the browser to attach the token automatically.

Security testing

@TestSecurity

io.quarkus.test.security.TestSecurity replaces the real authentication mechanism with a fixed, fake SecurityIdentity for a test method, so authorization logic can be exercised without standing up a real identity provider:

package com.example.book;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;

import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;

@QuarkusTest
class BookResourceSecurityTest {

    @Test
    @TestSecurity(user = "alice", roles = "editor")
    void editorCanCreate() {
        given()
                .when().post("/books")
                .then().statusCode(200);
    }

    @Test
    void anonymousCannotCreate() {
        given()
                .when().post("/books")
                .then().statusCode(401);
    }
}

@TestSecurity with OIDC

quarkus-test-security-oidc extends the same annotation to also populate the JsonWebToken/OidcSecurityIdentity claims a quarkus-smallrye-jwt- or quarkus-oidc-secured endpoint reads, via @OidcSecurity alongside @TestSecurity:

package com.example.book;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.OidcSecurity;

import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;

@QuarkusTest
class ClaimAwareResourceSecurityTest {

    @Test
    @TestSecurity(user = "alice", roles = "editor")
    @OidcSecurity(claims = {
            @Claim(key = "email", value = "alice@example.com")
    })
    void readsInjectedClaim() {
        given()
                .when().get("/books/owner-email")
                .then().statusCode(200).body(org.hamcrest.Matchers.is("alice@example.com"));
    }
}

This avoids running a real OIDC provider (or even Dev Services for Keycloak — see JWT, OIDC and Keycloak) just to unit-test that a resource method reads the right claim and enforces the right role.

Security customisation hooks

Beyond SecurityIdentityAugmentor and a custom HttpSecurityPolicy (both covered above), Quarkus Security exposes a few more extension points worth knowing exist: a custom HttpAuthenticationMechanism CDI bean to support a credential scheme none of the built-in extensions cover; a custom IdentityProvider to authenticate against a backing store none of quarkus-security-jdbc/quarkus-security-jpa/quarkus-elytron-security-ldap cover; and io.quarkus.security.spi.runtime.AuthorizationController, which can programmatically disable security checks entirely in a given environment (used internally by %test profiles in some extensions, and occasionally useful for a controlled security bypass in a diagnostic build) — reached for rarely, and never as a substitute for @TestSecurity in tests.

Vulnerability detection and reporting

Quarkus itself is scanned continuously and publishes advisories through the standard channels: security fixes are called out in each release announcement, and a suspected vulnerability in Quarkus or one of its extensions should be reported through GitHub’s private security-advisory form on the quarkusio/quarkus repository rather than a public issue, per Quarkus’s own security policy. Dependency-level vulnerabilities in a project’s own resolved dependency tree are a build-tooling concern (a CycloneDX SBOM plus a scanner such as OWASP Dependency-Check or Trivy against it) rather than a Quarkus Security feature as such — see the hardening checklist on Secrets, TLS and Hardening for where that fits into an overall security posture.

The authentication → identity → authorization pipeline

Three-stage pipeline diagram showing an HTTP request passing through authentication (HttpAuthenticationMechanism extracting credentials