Before adding OIDC to a JAX-RS application, let's distinguish two cases. A browser visits a protected page and needs to be redirected to a login form. An API caller, on the other hand, already has an access token and sends it with the request. The application must handle these differently.
The jax-rs-pac4j library (like pac4j) supports both. It protects resources with pac4j clients through annotations, on Jersey or RESTEasy (JAX-RS is now called Jakarta REST).
We'll start with browser login on Jersey 4, using an indirect client: OidcClient redirects the browser to the identity provider and completes the login on the callback endpoint. Then we'll protect a REST API with bearer tokens. We'll also see what changes for Jersey 3, RESTEasy and Dropwizard, whose pac4j bundle handles registration for us.
We'll create a small application from scratch.
What you need:
- Java 17 or later and Maven
- a Jakarta REST runtime: Jersey 3 or 4, RESTEasy 6 or 7, standalone on Grizzly or inside a servlet container, or Dropwizard 5
- an OpenID Connect provider where you can register an application, or the public demo server used below (
https://www.casserverpac4j.dev).
1) Create the Maven project
Create the directories for our application and its resources:
mkdir -p jaxrs-oidc-app/src/main/java/org/example/resources
cd jaxrs-oidc-app
Create a pom.xml at the project root. It targets Java 17, aligns the Jersey dependencies with a BOM and runs org.example.App with the Maven exec plugin:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>jaxrs-oidc-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jersey.version>4.0.2</jersey.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>${jersey.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Add the dependencies from section 2 here. -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.4</version>
<configuration>
<mainClass>org.example.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
For Jersey 3, use jersey.version v*3.1.12* and jersey3-pac4j in step 2. The Java code below is the same.
2) Add the Maven dependencies
Pick the jax-rs-pac4j module matching your runtime, and add the OpenID Connect module. The integration modules have the org.pac4j groupId and their version is 8.1.0 (it is based on pac4j v*6.5.9* as well):
| Your runtime | Maven artifact |
|---|---|
| Jersey 3.1 | jersey3-pac4j |
| Jersey 4.0 | jersey4-pac4j |
| RESTEasy 6.2 | resteasy6-pac4j |
| RESTEasy 7.0 | resteasy7-pac4j |
Add these dependencies inside the <dependencies> element of your pom.xml:
<!-- Jersey runtime and dependency injection -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>
<!-- embedded HTTP server -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
</dependency>
<!-- pac4j integration for Jersey 4 -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>jersey4-pac4j</artifactId>
<version>8.1.0</version>
</dependency>
<!-- pac4j support for OpenID Connect -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>6.5.9</version>
</dependency>
The pac4j integration declares the runtime in provided scope, so our standalone application supplies it explicitly above. The dependency guide lists the tested combinations.
3) Configure pac4j and register the features
First, build the Config with our OIDC client. We then need to tell JAX-RS three things:
- how pac4j should access requests and sessions
- how to process its security annotations
- how to inject profiles into resource methods.
Create src/main/java/org/example/App.java. It registers the runtime feature, security feature and profile value factory:
package org.example;
import java.net.URI;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.pac4j.core.config.Config;
import org.pac4j.jax.rs.features.Pac4JSecurityFeature;
import org.pac4j.jax.rs.grizzly.features.Pac4JGrizzlyFeature;
import org.pac4j.jax.rs.jersey.features.Pac4JValueFactoryProvider;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
public class App {
public static void main(final String[] args) throws InterruptedException {
final var baseUrl = "http://localhost:8080";
// configuration of the authentication via the OpenID Connect protocol
final var oidcConfiguration = new OidcConfiguration()
.setDiscoveryURI("https://www.casserverpac4j.dev/oidc/.well-known/openid-configuration")
.setClientId("myclient")
.setSecret("mysecret")
.setAllowUnsignedIdTokens(true);
final var config = new Config(baseUrl + "/callback", new OidcClient(oidcConfiguration));
final var application = new ResourceConfig()
.register(new Pac4JGrizzlyFeature(config)) // request context and session on Grizzly
.register(new Pac4JSecurityFeature()) // @Pac4JSecurity, @Pac4JCallback, @Pac4JLogout
.register(new Pac4JValueFactoryProvider.Binder()) // @Pac4JProfile, @Pac4JProfileManager
.packages("org.example.resources");
final var server = GrizzlyHttpServerFactory.createHttpServer(URI.create(baseUrl + "/"), application);
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow));
Thread.currentThread().join();
}
}
For OIDC, we set the discovery URI, client ID and secret as usual. Remove setAllowUnsignedIdTokens(true) when leaving the public demo server. The provider section of the Spring Boot guide covers registration at Keycloak, Google and Entra ID.
Our new Config(baseUrl + "/callback", ...) supplies the callback URL. Register it at the provider with the ?client_name=OidcClient suffix that pac4j appends.
The runtime choice matters for sessions. Here, Pac4JGrizzlyFeature uses Grizzly. Inside Tomcat, Jetty or WildFly, use Pac4JServletFeature(config) to access the container's HttpSession. Browser login needs a session to keep state between the redirect and the callback.
The profile injection is runtime-specific too: Pac4JValueFactoryProvider.Binder is for Jersey. With RESTEasy and CDI, register Pac4JSecurityFeature as a class and use Pac4JProfileInjectorFactory instead, following the RESTEasy configuration guide.
On Dropwizard, none of this registration code is needed: see the Dropwizard section below, then continue with step 4.
4) Declare the callback and logout endpoints
Create src/main/java/org/example/resources/AuthResource.java. The callback and logout look like ordinary resource methods with annotations. Their bodies never run, though: the pac4j filter processes the request and responds before JAX-RS reaches them.
package org.example.resources;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.pac4j.jax.rs.annotations.Pac4JCallback;
import org.pac4j.jax.rs.annotations.Pac4JLogout;
@Path("/")
public class AuthResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String home() {
return "Visit /protected/index to sign in, or /logout to sign out.";
}
@GET
@Path("callback")
@Pac4JCallback(defaultUrl = "/", renewSession = true)
public void callback() {
// handled by pac4j
}
@POST
@Path("callback")
@Pac4JCallback(defaultUrl = "/", renewSession = true)
public void callbackPost() {
// handled by pac4j, for the form_post response mode
}
@GET
@Path("logout")
@Pac4JLogout(destroySession = true, defaultUrl = "/")
public void logout() {
// handled by pac4j
}
}
Keep renewSession = true: pac4j rotates the session identifier after login to protect against session fixation. Version 8.1.0 fixes session renewal and preservation of session attributes on Grizzly, so there is no need to disable it for this setup.
5) Protect a resource and read the profile
Create src/main/java/org/example/resources/ProtectedResource.java. Annotate the resource method, or the whole class, with @Pac4JSecurity and name the client. Add a @Pac4JProfile parameter to receive the authenticated user:
package org.example.resources;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.pac4j.jax.rs.annotations.Pac4JProfile;
import org.pac4j.jax.rs.annotations.Pac4JSecurity;
import org.pac4j.oidc.profile.OidcProfile;
@Path("/protected")
public class ProtectedResource {
@GET
@Path("/index")
@Produces(MediaType.TEXT_PLAIN)
@Pac4JSecurity(clients = "OidcClient")
public String index(@Pac4JProfile OidcProfile profile) {
return "Hello " + profile.getDisplayName() + " (" + profile.getEmail() + ")"
+ "\nVisit /logout to sign out.";
}
}
When a user who has not signed in opens /protected/index, pac4j redirects their browser to the identity provider. After login, the provider returns the browser to /callback. pac4j validates the login, saves the user profile in the session and redirects the browser back to /protected/index.
pac4j then calls index(...) and supplies the authenticated user's OidcProfile through the @Pac4JProfile parameter. The two annotations have different jobs: @Pac4JSecurity protects the endpoint, while @Pac4JProfile gives the method access to the user's information.
You can extend @Pac4JSecurity with authorizers to check permissions, such as a required role, or with matchers to decide when security applies. These refer to authorizers and matchers configured in your Config.
There are other ways to access the profile, depending on what the method needs:
-
@Pac4JProfile CommonProfile profilegives access to fields shared across authentication mechanisms, such as the user ID. -
@Pac4JProfile Optional<CommonProfile> profilecan be empty when the endpoint's security configuration allows anonymous access. UsingOptionalalone does not allow anonymous access. -
@Pac4JProfileManager ProfileManager profileManagergives access to the profile manager, for example to retrieve all profiles associated with the request.
6) Protect a REST API with the access token
This step is an alternative to browser login. Let's take the second case: an API caller that already has an access token. For this setup, add org.pac4j:pac4j-http:6.5.9 for HeaderClient and a JSON provider such as jersey-media-json-jackson, matching your Jersey version. Also add this BOM inside <dependencyManagement><dependencies> to align the Jackson dependencies of Jersey and pac4j:
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.22.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Replace the stateful registration from step 3 with the configuration below.
Here we use a direct client, HeaderClient: the caller sends its access token in the Authorization: Bearer header, and authentication happens on that request. There is no browser redirect and no local session. HeaderClient checks the token at the OIDC provider's user info endpoint, using the OIDC client's profile creator.
In App.java, keep the OIDC configuration and server startup, replace the Config and ResourceConfig declarations, and add these imports:
import org.pac4j.http.client.direct.HeaderClient;
import org.pac4j.jax.rs.features.Pac4JJaxRsFeature;
import org.pac4j.jax.rs.pac4j.NoOpSessionStoreFactory;
final var oidcClient = new OidcClient(oidcConfiguration);
oidcClient.setCallbackUrl("notused");
oidcClient.init();
final var bearerClient = new HeaderClient("Authorization", "Bearer ", oidcClient.getProfileCreator());
final var config = new Config(bearerClient);
config.setSessionStoreFactory(NoOpSessionStoreFactory.INSTANCE);
final var application = new ResourceConfig()
.register(new Pac4JJaxRsFeature(config)) // no session at all
.register(new Pac4JSecurityFeature())
.register(new Pac4JValueFactoryProvider.Binder())
.packages("org.example.api");
Create src/main/java/org/example/api/MeResource.java:
package org.example.api;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.LinkedHashMap;
import java.util.Map;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.jax.rs.annotations.Pac4JProfile;
import org.pac4j.jax.rs.annotations.Pac4JSecurity;
@Path("/api/me")
public class MeResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Pac4JSecurity(clients = "HeaderClient")
public Map<String, Object> me(@Pac4JProfile CommonProfile profile) {
final Map<String, Object> user = new LinkedHashMap<>();
user.put("id", profile.getId());
user.put("email", profile.getEmail());
return user;
}
}
curl -H "Authorization: Bearer $ACCESS_TOKEN" http://localhost:8080/api/me
Restart the application, then set ACCESS_TOKEN to a real access token issued by your provider before running the command above. This checks credentials on every request. The provider must expose UserInfo and accept the access token there. We have identified the user, but we still need to enforce our API's audience, scopes and application-specific permissions. Configure these checks for your provider and API.
If the provider issues JWT access tokens, you can also validate them locally. Replace the profile creator with a JwtAuthenticator from pac4j-jwt, configured with the provider's JWKS and the expected issuer, audience and token lifetime checks.
An application can offer both browser login and bearer authentication. Keep a stateful configuration for the browser routes and a separate stateless one for the API: an API request must supply valid bearer credentials, even when the caller also has a browser session.
7) Logout
The /logout endpoint of step 4 performs the local logout. To also end the session at the identity provider, enable the central logout in the annotation:
@GET
@Path("logout")
@Pac4JLogout(destroySession = true, centralLogout = true, defaultUrl = "http://localhost:8080/")
public void logout() {
}
For a provider supporting OIDC logout, pac4j redirects to its end_session_endpoint. Register http://localhost:8080/ as an allowed post-logout redirect URI. If accepting a dynamic url parameter, also restrict it with logoutUrlPattern.
The stateless API has no local session to destroy. Stopping token use on the client does not revoke the token; it remains valid until expiry or provider-side revocation.
8) Run the application
mvn compile exec:java
For the browser configuration from steps 3–5, open http://localhost:8080/protected/index. You are redirected to the identity provider to sign in, then returned to the resource, which greets you by name.
If something goes wrong:
-
"Invalid redirect URI" at the provider: the registered URI must be the full callback URL, including
?client_name=OidcClient. -
The login loops or the state is lost: an indirect client such as
OidcClientneeds a session. UsePac4JGrizzlyFeatureorPac4JServletFeature, notPac4JJaxRsFeature, for the browser login. -
@Pac4JProfileis not injected: the value factory is missing. RegisterPac4JValueFactoryProvider.Binderon Jersey, orPac4JProfileInjectorFactoryon RESTEasy. -
401 on the API with a valid token: the provider rejects the token at the user info endpoint. Check that the token was issued for this provider and carries the
openidscope.
9) Using Dropwizard
What if your application uses Dropwizard? It runs Jersey inside Jetty, so the resource annotations above still apply. The dropwizard-pac4j bundle takes care of step 3: it builds Config from a factory named in YAML, registers the servlet and security features and the profile value factory, and enables Jetty sessions.
Create a separate dropwizard-oidc-app project with src/main/java/org/example/resources and src/main/java/org/example/security. Reuse the Maven skeleton from step 1, change its artifact ID to dropwizard-oidc-app and the exec plugin's main class to org.example.MyApplication.
Replace the Jersey BOM with io.dropwizard:dropwizard-bom:5.0.2 (also with type set to pom and scope to import), and replace the dependencies from step 2 with these:
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>dropwizard-pac4j</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>6.5.9</version>
</dependency>
Dropwizard 5 uses Jersey 3. The dropwizard-pac4j 8.1.0 bundle already brings jersey3-pac4j 8.1.0; keep the Jersey 4 dependencies out of this project. It also uses pac4j 6.5.9 and jakartaee-pac4j 8.0.3.
Create src/main/java/org/example/security/SecurityConfigFactory.java and move the OIDC configuration of step 3 into this ConfigFactory. Use the externally reachable callback URL, including any configured application context path.
By default, the bundle considers all JAX-RS requests as AJAX requests: an indirect client like OidcClient then returns a 401 error instead of redirecting to the identity provider, which suits REST APIs. For browser login on JAX-RS resources, restore the default pac4j behavior with a DefaultAjaxRequestResolver, as described in the bundle README:
package org.example.security;
import org.pac4j.core.config.Config;
import org.pac4j.core.config.ConfigFactory;
import org.pac4j.core.http.ajax.DefaultAjaxRequestResolver;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
public class SecurityConfigFactory implements ConfigFactory {
@Override
public Config build(final Object... parameters) {
final var oidcConfiguration = new OidcConfiguration()
.setDiscoveryURI("https://www.casserverpac4j.dev/oidc/.well-known/openid-configuration")
.setClientId("myclient")
.setSecret("mysecret")
.setAllowUnsignedIdTokens(true);
final var config = new Config("http://localhost:8080/callback", new OidcClient(oidcConfiguration));
// redirect the browser to the identity provider instead of returning a 401 error
config.getClients().setAjaxRequestResolver(new DefaultAjaxRequestResolver());
return config;
}
}
Create config.yml at the project root and reference the factory under a pac4j section:
pac4j:
configFactory: org.example.security.SecurityConfigFactory
Expose that section in your configuration class as a Pac4jFactory property, annotated with @Valid so that its content is validated, and add the bundle to the application:
package org.example;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.core.Configuration;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.pac4j.dropwizard.Pac4jFactory;
public class MyConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty("pac4j")
private Pac4jFactory pac4jFactory = new Pac4jFactory();
public Pac4jFactory getPac4jFactory() {
return pac4jFactory;
}
}
package org.example;
import io.dropwizard.core.Application;
import io.dropwizard.core.setup.Bootstrap;
import io.dropwizard.core.setup.Environment;
import org.example.resources.AuthResource;
import org.example.resources.ProtectedResource;
import org.pac4j.dropwizard.Pac4jBundle;
import org.pac4j.dropwizard.Pac4jFactory;
public class MyApplication extends Application<MyConfiguration> {
public static void main(final String[] args) throws Exception {
new MyApplication().run(args);
}
private final Pac4jBundle<MyConfiguration> pac4j = new Pac4jBundle<>() {
@Override
public Pac4jFactory getPac4jFactory(final MyConfiguration configuration) {
return configuration.getPac4jFactory();
}
};
@Override
public void initialize(final Bootstrap<MyConfiguration> bootstrap) {
bootstrap.addBundle(pac4j);
}
@Override
public void run(final MyConfiguration configuration, final Environment environment) {
environment.jersey().register(AuthResource.class);
environment.jersey().register(ProtectedResource.class);
}
}
Save the configuration and application classes as src/main/java/org/example/MyConfiguration.java and src/main/java/org/example/MyApplication.java. Copy AuthResource and ProtectedResource from steps 4 and 5 into the resource directory. Their callbacks already enable session renewal; Dropwizard uses servlet sessions.
Start this application with its YAML configuration:
mvn compile exec:java -Dexec.args="server config.yml"
Open http://localhost:8080/protected/index to follow the same browser login flow.
You can also declare a global filter (globalFilters, which accepts only one entry) in the pac4j section to protect the whole API, or servlet-level filters for the non-Jersey parts of the application. The bundle README describes these options.
For the bearer-token API from step 6, set sessionEnabled: false, build the HeaderClient in the factory and, as in step 6, disable the session store with config.setSessionStoreFactory(NoOpSessionStoreFactory.INSTANCE). The dropwizard-pac4j-demo protects Dropwizard views with form, HTTP Basic and CAS logins.
10) Switching to SAML or CAS
For browser login, add pac4j-saml or pac4j-cas, replace the OidcClient and update @Pac4JSecurity. Adapt the injected profile type or use CommonProfile, and register the protocol-specific callback and logout settings. SAML also needs a keystore and metadata exchange. The bearer-token example is a separate authentication mechanism and is not converted by changing this browser client. The protocol-specific setup is described in the SAML guide and the CAS guide.
11) Learn more
- The jax-rs-pac4j library and its documentation, including the Servlet, Grizzly, CDI and sessionless setups.
- The jax-rs-pac4j-demo application, and the release post on Jersey 4 and RESTEasy 7 support.
- The dropwizard-pac4j bundle and the dropwizard-pac4j-demo application.
- The documentation for the OIDC client for Java for the complete client configuration.
- Direct OIDC authentication and JWT validation for the access token case.
Discover more pac4j frameworks and more authentication mechanisms…
Top comments (0)