JWT, OIDC and Keycloak
|
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. |
This page covers Quarkus’s two token-based security extensions — quarkus-smallrye-jwt and quarkus-oidc — and how each fits into the shared SecurityIdentity model from
Security Overview and Architecture. It documents
Quarkus-specific configuration, annotations, and tooling only: what a JWT is, how OAuth 2.0’s authorization-code
and client-credentials flows work, what a JWKS endpoint is for, and how OpenID Connect layers authentication on
top of OAuth are all protocol-level theory covered in depth by the sibling
OAuth Reference — in particular
JWT and the JOSE Family,
OpenID Connect, and
Authorization Code and PKCE — and is linked from here
rather than repeated.
quarkus-smallrye-jwt: MicroProfile JWT RBAC
quarkus-smallrye-jwt implements the MicroProfile
JWT RBAC specification: it verifies an incoming bearer JWT and turns its claims directly into role-based
authorization, without any separate OIDC discovery/session machinery.
./mvnw quarkus:add-extension -Dextensions=smallrye-jwt
Verification keys
mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://issuer.example.com
smallrye.jwt.path.groups=groups
mp.jwt.verify.publickey.location points at a PEM-encoded public key (or a JWKS URL, for a provider that
publishes one) used to verify the token’s signature; mp.jwt.verify.issuer rejects any token whose iss claim
does not match, and smallrye.jwt.path.groups tells SmallRye JWT which claim to read roles from when the token
does not use the MicroProfile-standard groups claim name.
JsonWebToken and @Claim injection
package com.example.book;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/books")
public class BookResource {
@Inject
JsonWebToken jwt;
@Inject
@Claim("email")
String email;
@GET
@Path("/me")
@RolesAllowed("user")
public String me() {
return jwt.getName() + " <" + email + ">, expires " + jwt.getExpirationTime();
}
}
org.eclipse.microprofile.jwt.JsonWebToken (itself a java.security.Principal) exposes the whole claim set
programmatically (getClaim(String), getGroups(), getExpirationTime()); @Claim injects one named claim
directly as a field or constructor parameter, typed as the claim’s natural Java type (String, Long,
Set<String>, or JsonValue for anything structured) — the same trade-off as @ConfigProperty vs.
@ConfigMapping on the Configuration page: convenient for one value,
less structured than reading the whole JsonWebToken for many.
Building, signing and encrypting tokens with the JWT build API
smallrye-jwt-build (pulled in transitively by quarkus-smallrye-jwt) issues tokens instead of only verifying
them — useful for a test fixture, an internal token-issuing endpoint, or a service that mints a short-lived
token for a downstream call:
package com.example.book;
import io.smallrye.jwt.build.Jwt;
public class TokenIssuer {
public String issueAccessToken(String subject, String... groups) {
return Jwt.issuer("https://issuer.example.com")
.subject(subject)
.groups(java.util.Set.of(groups))
.expiresIn(java.time.Duration.ofMinutes(15))
.sign();
}
public String issueEncryptedToken(String subject) {
return Jwt.claims()
.subject(subject)
.jwe()
.keyEncryptionKeySecret("0123456789ABCDEF0123456789ABCDEF".getBytes())
.encrypt();
}
}
.sign() produces a signed JWS (using the private key configured under smallrye.jwt.sign.key.location, the
counterpart to mp.jwt.verify.publickey.location); .jwe()…encrypt() instead produces an encrypted JWE. Both
build on the same JOSE mechanics covered on JWT and the JOSE Family.
quarkus-oidc
quarkus-oidc is the extension for delegating authentication (and, in service mode, token validation) to an
external OpenID Connect provider — Keycloak, or a well-known provider (see
Well-known provider configuration) — rather than verifying tokens locally the way quarkus-smallrye-jwt
does above.
./mvnw quarkus:add-extension -Dextensions=oidc
Web-app mode
Web-app mode is for a browser-facing application: quarkus-oidc drives the OAuth 2.0 authorization-code flow
itself, storing the resulting tokens in an encrypted session cookie rather than handing them to application
code directly.
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/example
quarkus.oidc.client-id=book-webapp
quarkus.oidc.credentials.secret=${oidc-client-secret}
quarkus.oidc.application-type=web-app
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/
application-type=web-app is what switches quarkus-oidc into this mode; hitting any endpoint protected by
@Authenticated/@RolesAllowed with no valid session redirects the browser into the provider’s login page,
and a successful callback establishes the session cookie. quarkus.oidc.logout.path wires an endpoint that
clears the local session and redirects to the provider’s own end-session endpoint, landing back at
post-logout-path afterward — session/logout mechanics themselves (front-channel vs. back-channel logout,
single sign-out) are protocol-level and covered on
Logout and Session Management.
Service mode
Service mode is for a machine-facing API that expects a bearer token on every request and performs no redirect of its own:
quarkus.oidc.auth-server-url=https://keycloak.example.com/realms/example
quarkus.oidc.client-id=book-service
quarkus.oidc.application-type=service
quarkus.oidc.token.issuer=https://keycloak.example.com/realms/example
By default, quarkus-oidc validates a bearer JWT against the provider’s published JWKS (fetched from the
`auth-server-url’s discovery document and cached, refreshed on a configurable interval or on a signing-key-id
miss). Switching to token introspection instead — required for an opaque, non-JWT access token, or when the
provider’s revocation semantics demand it — is a property away:
quarkus.oidc.token.introspection-required=true
quarkus.oidc.introspection-credentials.name=book-service
quarkus.oidc.introspection-credentials.secret=${oidc-introspection-secret}
A resource method in service mode reads the validated identity the same way as any other Quarkus Security
mechanism — SecurityIdentity, JsonWebToken, or @Claim, exactly as under quarkus-smallrye-jwt above — since both extensions converge on the same SecurityIdentity/JsonWebToken types once validation succeeds.
Tenant resolution and multitenancy
A single Quarkus application can serve more than one OIDC tenant — different realms, different providers, or a
mix of web-app and service configuration for different paths — by declaring named tenants under
quarkus.oidc."<tenant>".* and resolving which one applies per request:
quarkus.oidc.tenant-a.auth-server-url=https://keycloak.example.com/realms/tenant-a
quarkus.oidc.tenant-a.client-id=tenant-a-app
quarkus.oidc.tenant-a.application-type=web-app
quarkus.oidc.tenant-b.auth-server-url=https://keycloak.example.com/realms/tenant-b
quarkus.oidc.tenant-b.client-id=tenant-b-app
quarkus.oidc.tenant-b.application-type=service
A custom io.quarkus.oidc.TenantConfigResolver or io.quarkus.oidc.TenantResolver CDI bean picks the active
tenant per request (from a path segment, a header, or a subdomain), or the simpler
quarkus.oidc.tenant-a.tenant-paths=/tenant-a/* path-based resolution handles the common case of routing by URL
prefix without any custom code.
Token propagation and service-to-service calls
The OIDC client for outbound tokens
quarkus-oidc-client obtains and refreshes an access token independently of any inbound request — for a
scheduled job or a service that needs to call another API using its own client-credentials identity rather than
a caller’s token:
./mvnw quarkus:add-extension -Dextensions=oidc-client
quarkus.oidc-client.auth-server-url=https://keycloak.example.com/realms/example
quarkus.oidc-client.client-id=book-batch-job
quarkus.oidc-client.credentials.secret=${oidc-client-secret}
quarkus.oidc-client.grant.type=client
package com.example.book;
import io.quarkus.oidc.client.OidcClient;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@ApplicationScoped
public class InventorySyncJob {
@Inject
OidcClient oidcClient;
@Inject
@RestClient
InventoryClient inventoryClient;
public Uni<String> syncOnce() {
return oidcClient.getTokens()
.onItem().transformToUni(tokens ->
inventoryClient.pullInventory("Bearer " + tokens.getAccessToken()));
}
}
quarkus-oidc-token-propagation for forwarding the caller’s token
When a service should instead forward the caller’s own bearer token to a downstream call — rather than authenticating as itself — quarkus-oidc-token-propagation (paired with a
Quarkus REST Client) attaches the current request’s access token to an
outbound @RestClient call automatically, via the @AccessToken/reactive filter it registers, with no manual
header-copying code needed.
quarkus-oidc-db-token-state-manager and token-state options
In web-app mode, quarkus-oidc must remember each session’s tokens somewhere between requests. By default that
state lives inside the encrypted session cookie itself (quarkus.oidc.token-state-manager.strategy=keep-all-tokens,
the default) — simple, but it grows the cookie with every token stored and puts token material in the
browser. quarkus-oidc-db-token-state-manager instead persists token state server-side, in a relational
database, and keeps only a session reference in the cookie:
./mvnw quarkus:add-extension -Dextensions=oidc-db-token-state-manager
Other quarkus.oidc.token-state-manager.strategy values narrow what the cookie holds without moving state
server-side: id-refresh-tokens drops the access token from the cookie (it is re-derived from the refresh
token as needed), and id-token keeps only the ID token, appropriate when a web app never needs to make an
authenticated call of its own and only needs to know who is logged in.
Dev Services for Keycloak and the OIDC Dev UI
With quarkus-oidc (or quarkus-keycloak-authorization) on the classpath and no quarkus.oidc.auth-server-url
configured, Dev Services automatically starts a disposable Keycloak container under quarkus:dev/./mvnw test — pre-provisioned with a realm, a client, and test users, so local development and integration tests need no
externally running identity provider:
quarkus.keycloak.devservices.realm-path=my-realm.json
quarkus.keycloak.devservices.port=8180
realm-path imports a realm export instead of Dev Services' own default realm, for tests that need specific
clients, roles, or users pre-provisioned. In dev mode, the Dev UI's OIDC page
shows the current tenant configuration, lets a developer obtain a token interactively (username/password against
the Dev Services realm, or a client-credentials grant) and inspect its claims, without leaving the browser or
running a separate curl/Postman request against the token endpoint.
Well-known provider configuration
quarkus.oidc.provider shortcuts discovery/endpoint configuration for a recognized identity provider, filling
in URLs and provider-specific quirks that would otherwise need to be copied from that provider’s own
documentation:
quarkus.oidc.provider=google
quarkus.oidc.client-id=${google-client-id}
quarkus.oidc.credentials.secret=${google-client-secret}
Supported values include google, github, apple, microsoft (Entra ID/Azure AD), and auth0, each mapping
onto that provider’s own OIDC/OAuth endpoints and any non-standard behavior (GitHub, notably, is OAuth-only with
no OIDC discovery document, which provider=github accounts for internally). Social-login federation, account
linking, and per-provider claim-mapping trade-offs are protocol-level topics covered on
Social Login and Federation; this property is purely a
Quarkus configuration shortcut on top of that theory. For configuring Microsoft Entra ID as the OIDC provider
in depth — app registrations, RBAC and managed identities — see Identity and Access.
The legacy quarkus-elytron-security-oauth2 opaque-token path
quarkus-elytron-security-oauth2 predates quarkus-oidc and validates an opaque bearer token purely via
introspection, with none of `quarkus-oidc’s discovery, web-app/session, or multitenancy support:
quarkus.security.oauth2.enabled=true
quarkus.security.oauth2.client-id=book-service
quarkus.security.oauth2.client-secret=${oauth2-client-secret}
quarkus.security.oauth2.introspection-url=https://provider.example.com/oauth2/introspect
It remains usable, but quarkus-oidc in service mode with introspection-required=true (see Service mode
above) covers the same opaque-token scenario with active maintenance, OIDC discovery, and the rest of this
page’s tooling (Dev Services, the Dev UI, well-known providers) available on top — new code should reach for
that instead, treating quarkus-elytron-security-oauth2 as a compatibility path for an existing project that
already depends on it. Opaque tokens, introspection semantics, and revocation are covered at the protocol level
on Opaque Tokens, Introspection and
Revocation.