You want users to sign in to your Java application with Keycloak, Google, Microsoft Entra ID, Okta, Auth0 or even a CAS server. They already have an account there, so your application should not ask them for another password.
This is what OpenID Connect (OIDC) is for. It adds an identity layer on top of OAuth 2.0: the provider authenticates the user, and your application receives an ID token, a signed JWT describing who signed in. The user's password stays with the provider.
Let's put this into practice with pac4j and Spring Boot. We'll start with the public pac4j test server, then see what to change for Keycloak, Google or Azure AD. The same approach applies to other OpenID Connect providers.
What you need:
- Java 17+ and Maven
- an OpenID Connect provider where you can register an application, or the public demo server used below.
1) Create the Maven project
Create an empty Spring Boot project with Java 17:
mkdir -p spring-oidc-app/src/main/java/org/example
mkdir -p spring-oidc-app/src/main/resources
cd spring-oidc-app
Create a pom.xml file at the project root. The Spring Boot parent manages the Spring dependencies, and its Maven plugin runs the application:
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>org.example</groupId>
<artifactId>spring-oidc-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Add the dependencies from section 2 here. -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Save the Java classes below in the org.example package so that Spring Boot discovers the configuration and controllers. Put the application settings in src/main/resources/application.yml.
2) Add the Maven dependencies
Inside the <dependencies> element, on top of Spring MVC, you need two pac4j artifacts: the Spring MVC integration and the OpenID Connect module.
<!-- Spring Boot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- pac4j implementation for Spring MVC so for Spring Boot as well -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>spring-webmvc-pac4j</artifactId>
<version>8.0.4</version>
</dependency>
<!-- pac4j support for OpenID Connect -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>6.5.9</version>
</dependency>
Why two dependencies? pac4j-oidc handles the protocol, built on the Nimbus OAuth 2.0 / OpenID Connect SDK, while spring-webmvc-pac4j connects it to Spring MVC. The OidcClient itself knows nothing about Spring: you can use it with Jakarta EE, Play, Vert.x, JAX-RS and the other pac4j integrations.
3) Configure OpenID Connect (OIDC) authentication
Create src/main/java/org/example/SecurityConfig.java:
package org.example;
import org.pac4j.core.config.Config;
import org.pac4j.springframework.config.Pac4jSecurityConfig;
import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@Configuration
public class SecurityConfig extends Pac4jSecurityConfig {
@Value("${app.base-url:http://localhost:8080}")
private String baseUri;
@Bean
public Config config() {
// configuration of the authentication via the OpenID Connect protocol
final var config = new OidcConfiguration()
.setDiscoveryURI("https://www.casserverpac4j.dev/oidc/.well-known/openid-configuration")
.setClientId("myclient")
.setSecret("mysecret")
.setAllowUnsignedIdTokens(true);
return new Config(baseUri + "/callback", new OidcClient(config));
}
@Override
public void addInterceptors(final InterceptorRegistry registry) {
// the /protected/** URLs require the OIDC authentication
addSecurity(registry, "OidcClient").addPathPatterns("/protected/**");
}
}
There are two things to configure here: the OIDC client and the URLs of our application.
For the client, setDiscoveryURI points to the provider's .well-known/openid-configuration document. pac4j reads the authorization, token, user info and JWKS endpoints from it. So you do not have to configure each endpoint yourself. setClientId and setSecret are the credentials you receive when registering the application at the provider.
On the application side, Pac4jSecurityConfig registers /callback and /logout. The callback URL comes from new Config(baseUri + "/callback", ...), and pac4j appends ?client_name=OidcClient to it. This full URL is the redirect URI to register at your provider.
Finally, addSecurity(registry, "OidcClient") protects /protected/**. An anonymous user requesting one of these pages is redirected to the provider to log in.
By default, pac4j uses the authorization code flow and adds PKCE when the provider advertises support for it in its discovery document.
You may wonder why the example allows unsigned ID tokens. This is only because the public demo server issues them. Remove setAllowUnsignedIdTokens(true) for a real provider, so pac4j verifies the ID token signature against the provider's JWKS.
4) Register the application at your identity provider
Every provider needs the same three things: an application (or "client") with a redirect URI of http://localhost:8080/callback?client_name=OidcClient, and the resulting client ID and secret. What changes is where the discovery document lives, and pac4j ships dedicated clients for the common providers.
- For Keycloak: create a confidential client in your realm and set its "Valid redirect URIs". The KeycloakOidcConfiguration builds the discovery URL from the server base URL and the realm name:
final var config = new KeycloakOidcConfiguration()
.setBaseUri("https://keycloak.example.com")
.setRealm("myrealm");
config.setClientId("myclient");
config.setSecret("mysecret");
return new Config(baseUri + "/callback", new KeycloakOidcClient(config));
- For Google: create an "OAuth client ID" of type "Web application" in the Google Cloud console and add the redirect URI to "Authorized redirect URIs". The GoogleOidcClient already knows Google's discovery URL:
final var config = new OidcConfiguration()
.setClientId("xxx.apps.googleusercontent.com")
.setSecret("GOCSPX-...");
return new Config(baseUri + "/callback", new GoogleOidcClient(config));
- For Microsoft Entra ID (Azure AD): register an application in the Entra portal, add the redirect URI under "Web" platform and create a client secret. The AzureAd2Client takes the tenant identifier:
final var config = new AzureAd2OidcConfiguration("38c46e5a-21f0-46e5-940d-3ca06fd1a330");
config.setClientId("788339d7-1c44-4732-97c9-134cb201f01f");
config.setSecret("...");
return new Config(baseUri + "/callback", new AzureAd2Client(config));
For Okta, Auth0, a CAS server or any other provider, the generic OidcClient with the provider's discovery URL is all you need. Replace client_name=OidcClient in the redirect URI with the name of the client you use (KeycloakOidcClient, GoogleOidcClient, AzureAd2Client).
Update the security interceptor as well: the name passed to addSecurity must match the configured client. For example, with KeycloakOidcClient, replace the interceptor configuration from step 3 with:
addSecurity(registry, "KeycloakOidcClient").addPathPatterns("/protected/**");
Use GoogleOidcClient or AzureAd2Client in both places for the corresponding provider.
5) Access the authenticated user
Create src/main/java/org/example/Application.java to expose a public page and a protected page:
package org.example;
import org.pac4j.core.profile.ProfileManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Application {
@Autowired
private ProfileManager profileManager;
@GetMapping(value = "/", produces = "text/html")
public String index() {
return "<h1>Public area</h1><p><a href='/protected/index'>Protected area</a></p>"
+ "<p><a href='/logout'>Logout</a></p>";
}
@GetMapping(value = "/protected/index", produces = "text/plain")
public String secure() {
return "Protected area\n" + profileManager.getProfiles() + "\nVisit /logout to sign out.";
}
}
Now that the user is authenticated, how do we get their information? The ProfileManager reads the profile saved in the session. For OIDC, this is an OidcProfile, with getters for the standard claims and access to the raw tokens:
final var profile = (OidcProfile) profileManager.getProfile().orElseThrow();
profile.getId(); // the "sub" claim
profile.getEmail();
profile.getDisplayName();
profile.getAttribute("preferred_username");
profile.getIdToken(); // the parsed ID token (JWT)
profile.getAccessToken(); // to call the provider's APIs
Which claims are present depends on the scopes you request. The default is openid profile email; add more with config.setScope("openid profile email phone").
6) Logout
The /logout link removes the profile from the application's session. This is the local logout: the user may still be logged in at the provider.
To end that session as well, enable central logout in application.yml:
pac4j:
logout:
centralLogout: true
defaultUrl: http://localhost:8080/
For providers supporting OIDC logout, pac4j redirects the browser to the end_session_endpoint from the discovery document and supplies the absolute default URL as post_logout_redirect_uri. Register http://localhost:8080/ as an allowed post-logout redirect URI at the provider. A relative default URL such as / works for local logout but is not passed to the provider as a return URL.
If your provider supports OIDC logout but does not publish its endpoint, set it explicitly with config.setLogoutUrl(...). Logout support and registration requirements vary by provider; the dedicated GoogleOidcClient uses its own logout action rather than this discovery-based flow.
7) Run the application
Create src/main/java/org/example/SpringBootDemo.java:
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootDemo {
public static void main(final String[] args) {
SpringApplication.run(SpringBootDemo.class, args);
}
}
Start the application with mvn spring-boot:run.
Open http://localhost:8080/ and follow Protected area. You are redirected to the identity provider to sign in, then returned to the protected page, where the controller prints your profile.
If something goes wrong:
-
"Invalid redirect URI" or "redirect_uri_mismatch" at the provider: the registered URI must be the full callback URL, including
?client_name=OidcClient(or the name of the dedicated client configured in step 4). - "invalid_client" on the token request: wrong client secret, or the client is public at the provider while pac4j sends a secret.
-
The ID token signature fails: check that discovery points to the intended provider, that its JWKS is reachable and contains the current signing key, and that the expected signing algorithm matches the provider's client configuration. If an explicit algorithm is needed, use
config.setIdTokenSigningAlgorithm(...)with that configured algorithm; changing it toES256is not a general fix for signature errors. - Empty name or email: the scopes or the provider's claim mapping do not release them. Check the scopes first, then the client configuration at the provider.
8) Learn more
- The Spring Boot OIDC demo for a complete reference application.
- The documentation for the OIDC client for Java: all the
OidcConfigurationoptions, the implicit flow,private_key_jwtclient authentication, nonce and state handling. - Direct OIDC authentication to protect a REST API with the access tokens issued by your provider.
- OpenID Federation when your application belongs to a trust federation.
Discover more pac4j frameworks and more authentication mechanisms…
Top comments (0)