How to secure a Java application with OIDC (using Spring Boot)

OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0. Your Java application never sees the user’s password: it redirects the browser to an identity provider (Keycloak, Google, Microsoft Entra ID, Okta, Auth0, a CAS server…), the provider authenticates the user and sends back an ID token, a signed JWT that tells your application who signed in.

This guide uses pac4j with Spring Boot to add OIDC login to a Java web application in seven steps. It works with any OpenID Connect provider: the demo points to the public pac4j test server, and a dedicated section shows what changes for Keycloak, Google and Azure AD.

What you need:

1) Get the Spring Boot demo

The OIDC demo project contains the three classes shown in this guide, ready to run:

git clone --branch oidc --single-branch https://github.com/pac4j/simple-spring-boot-pac4j-demos.git
cd simple-spring-boot-pac4j-demos

2) Add the Maven dependencies

The demo’s pom.xml uses the Spring Boot parent. 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.3</version>
</dependency>
<!-- pac4j support for OpenID Connect -->
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-oidc</artifactId>
    <version>6.5.8</version>
</dependency>

The pac4j-oidc module is framework-agnostic: the same OidcClient works with Jakarta EE, Play, Vert.x, JAX-RS and the other pac4j integrations. The spring-webmvc-pac4j dependency provides the Spring MVC integration used by this Spring Boot application.

3) Configure OpenID Connect (OIDC) authentication

The whole security setup fits in one class, SecurityConfig:

@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/**");
    }
}

What each part does:

By default pac4j uses the authorization code flow, and it adds PKCE automatically when the provider advertises support for it in its discovery document. setAllowUnsignedIdTokens(true) only exists because the public demo server issues unsigned ID tokens: remove it for any real provider, so that the ID token signature is always verified 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.

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));

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));

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

The application controller exposes a public page and a protected page:

@Autowired
private ProfileManager profileManager;

@RequestMapping("/")
@ResponseBody
public String index() {
    return "<h1>Public area</h1><p><a href='/protected/index'>Protected area</a></p>"
            + "<p><a href='/logout'>Logout</a></p>" + profileManager.getProfiles();
}

@RequestMapping("/protected/index")
@ResponseBody
public String secure() {
    return "<h1>Protected area</h1><a href='/'>Home</a><p/>"
            + "<p><a href='/logout'>Logout</a></p>" + profileManager.getProfiles();
}

The ProfileManager gives you the profile saved in the session after login. For OpenID Connect it is an OidcProfile, which exposes the standard claims as typed getters and keeps 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 created by the integration removes the profile from the session: this is the local logout. To also end the session at the identity provider, enable the central logout in application.properties:

pac4j.logout.centralLogout=true
pac4j.logout.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

Start SpringBootDemo from your IDE or with mvn spring-boot:run:

@SpringBootApplication
public class SpringBootDemo {
    public static void main(final String[] args) {
        SpringApplication.run(SpringBootDemo.class, args);
    }
}

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:

Learn more

Using a different integration? Reuse the OIDC client configuration with Jakarta EE servlet filters, or connect pac4j authentication to an existing Spring Security application.

Discover more pac4j frameworks and more authentication mechanisms