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 |
|---|---|
|
Allows the call only if the current |
|
Allows the call for any non-anonymous identity, regardless of role — shorthand for
|
|
Allows the call unconditionally, including for the anonymous identity. |
|
Rejects the call unconditionally, even for an authenticated identity with every role — useful for disabling
an endpoint without deleting it. |
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.