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.

Sequence diagrams

Web-app mode: authorization-code flow

sequenceDiagram participant B as Browser participant Q as Quarkus app (quarkus-oidc, web-app) participant K as OIDC provider (Keycloak) B->>Q: GET /books (no session cookie) Q-->>B: 302 redirect to K authorization endpoint B->>K: GET /authorize?... K-->>B: login page B->>K: submit credentials K-->>B: 302 redirect to Q callback with code B->>Q: GET /callback?code=... Q->>K: POST /token (exchange code for tokens) K-->>Q: access_token, id_token, refresh_token Q-->>B: Set-Cookie (encrypted session), 302 to /books B->>Q: GET /books (session cookie) Q-->>B: 200 books

Service mode: bearer-token validation

sequenceDiagram participant C as API client participant Q as Quarkus app (quarkus-oidc, service) participant K as OIDC provider (Keycloak) C->>K: POST /token (client credentials or code exchange) K-->>C: access_token (JWT) C->>Q: GET /books (Authorization: Bearer access_token) Q->>K: GET /.well-known/openid-configuration + JWKS (cached after first call) K-->>Q: JWKS (signing keys) Q->>Q: verify signature, issuer, expiry locally Q-->>C: 200 books