Authentication via OpenID Connect

published: ⏰ 4

A quick example of OpenID Connect authentication for a Spring Boot application with a UI. I couldn’t find a complete example (frontend and backend), so I decided to add this article as a reference. I might add more to it later.

Introduction

What’s the gist? We need to create an application that will use OpenID Connect to authenticate users. The application has a UI (ReactJS), and we need to enable users to log in via an OIDC provider. This can be any provider, not tied to a specific vendor.

What the example will consist of:

  • A Spring Boot application with Web MVC.
  • A library for client-side OIDC.
  • OAuth2 authorization via OIDC using Spring Security.
  • Keycloak as the OIDC provider, but any other provider can be used.

Server-Side

The server-side is a Spring Boot application that will use Spring Security to authenticate users via OIDC.

The server side is pretty straightforward. The easiest way to create the required Spring Boot application is with Spring Initializr. Select the following dependencies:

  • org.springframework.boot:spring-boot-starter-web.
  • org.springframework.boot:spring-boot-starter-security.
  • org.springframework.boot:spring-boot-starter-oauth2-resource-server – для работы с JWT токенами.

Next, you need to configure Spring Security.

Java
public class SecurityConfig implements WebMvcConfigurer {

    private final OidcJwtConverter jwtConverter;

    @Bean
    SecurityFilterChain demoSecurityFilterChain(final HttpSecurity http) throws Exception {
        return http.oauth2ResourceServer(o -> o.jwt(jwt
                        -> jwt.jwtAuthenticationConverter(jwtConverter)))
                .build();
    }
}

The OidcJwtConverter class is responsible for converting a JWT token into a Spring Security authentication token – org.springframework.security.core.Authentication.

Java

@Component
public class OidcJwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {

    private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter 
            = new JwtGrantedAuthoritiesConverter();

    @Override
    public AbstractAuthenticationToken convert(final Jwt jwt) {
        final var expiresAt = jwt.getExpiresAt() == null ? 0 : jwt.getExpiresAt().toEpochMilli();
        if (System.currentTimeMillis() >= expiresAt) {
            SecurityContextHolder.getContext().setAuthentication(null);
            return null;
        }

        final var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
        final var identity = (String) jwt.getClaim(JwtClaimNames.SUB);

        // {extra logic}

        return new JwtAuthenticationToken(jwt, authorities, identity);
    }
}

Here, {extra logic} is additional logic that can be implemented within a specific project:

  • Checking for user blocking.
  • Automatic user or profile registration. For example, for cases where roles and permissions are assigned not on the IdP side, but in the application.
  • Retrieving a list of user roles and permissions, for example, from a database.

Additionally, it’s worth caching authentication tokens, since in the case of a stateless service, they will be checked and populated with each request. This can be quite expensive when downloading from the database. You can cache them either in memory (if deploying without a cluster) or in an external cache (Hazelcast, Redis, Infinispan, etc.).

Client-Side

On the client side, there are several ready-made OIDC client implementations (only a few of them are listed here):

I didn’t want to be tied to a specific provider, so I used oidc-client-ts . The react-oidc-context wrapper for ReactJS is based on this library. You can use it if you don’t have the time or inclination to deal with the main library.

The main authorization management class is UserManager . It is responsible for:

  • Obtaining tokens (access, refresh).
  • Refreshing tokens.
  • Obtaining information about the logged-in user.
  • Resetting authorization (logout).

An example configuration is below:

TypeScript
import {UserManager} from 'oidc-client-ts';

const settings = {
    authority: 'http://localhost:9080/realms/my-realm',
    client_id: 'my-client-id',
    redirect_uri: 'http://localhost:5173/callback',
    response_type: 'code',
    scope: 'openid profile email api',
    post_logout_redirect_uri: 'http://localhost:5173/login',
};

const userManager = new UserManager(settings);
  • authority – URL of the authorization server (here, it’s Keycloak).
  • client_id – Client ID.
  • redirect_uri – URI to which the user will be redirected after successful authorization.
  • response_type – Type of response from the authorization server. In this case, code (Authorization Code Flow) is used.
  • scope – List of requested access scopes.
  • post_logout_redirect_uri – URI to which the user will be redirected after logging out.

Required methods:

  • userManager.getUser() – retrieves information about the current user.
  • userManager.signinRedirect() – redirects the user to the authorization page.
  • userManager.signinCallback() – handles the authorization result. Returns user information.
  • userManager.signoutRedirect() – ends the user session and redirects to the logout page.

The return URI from the authorization server can be made as a separate page (callback) to call the signinCallback method there and save the retrieved user locally. This method can only be called once after returning, as it removes the request information from localStorage. After successful authorization, the user can be redirected from this callback to the main page.

Using Access Tokens

To use tokens when accessing the server API (REST), you must add the Authorization header in the format Bearer in each request.

If you are using axios, you can add the standard headers axios.defaults.headers.common[‘Authorization’] = ‘Bearer ’ + token; .

Alternatively, you can use interceptors to dynamically add a header to each request based on specific logic.

TypeScript
axios.create({...}).interceptors.request.use(
    (config) => {
        if (oidc.isAuthenticated()) {
            config.headers.Authorization = `Bearer ${oidcProtocol.getToken()}`;
        }
        return config;
    },
    (error: AxiosError) => {
        return Promise.reject(error);
    },
);

Identity Provider (IdP)

This example uses Keycloak. The setup won’t be shown, as it’s relatively simple. You need to:

  1. Create a client with Standard flow. All settings are default, just specify the correct URIs in the Valid * fields.
  2. Create a user.

Conclusion

Obviously, this is only a basic implementation, but it allows you to complete the authentication process via OpenID Connect and obtain a token for further use in API requests.

PS: You can use react-oidc-context instead of oidc-client-ts to avoid rewriting the logic for ReactJS. Since I need my own implementation for tekBlueprint.frame, I used oidc-client-ts.

ℹ️ Note

There is no example project yet, maybe there will be later.

Java Security Oidc Oauth2 Jwt Spring