JVM Advent

The JVM Programming Advent Calendar

A new Spring Security authorization server

In this article, we discuss the new Spring authorization server framework. This framework comes as a replacement for the capability to build an OAuth authorization server after the deprecation of the Spring Security OAuth project. The Spring Security OAuth project needed to be deprecated. It was only offering support for the OAuth 1 specification, and building a reliable OAuth 2/OpenID Connect using it was not easy. The part that refers to the client and the resource server moved directly into Spring Security, while the authorization server part has been extracted into a separate framework, which we’ll discuss in this article. After reading this article, please visit and find more details directly on the project’s main page: Spring Security Authorization Server

1. What is an authorization server

Let’s start with a briefing on OAuth 2 and what an authorization server is. In most organizations, people work with multiple software systems which cover different purposes in their day-to-day work. And I’m certain that even you are not comfortable having multiple accounts for each and every software system you use. We’d prefer a better way to manage access. With an OAuth 2 implementation, we have a separate component that manages the users credentials. We call this component the authorization server. Any (frontend) application would need to request the user to log in through the authorization server to access its backend. So instead of having each application managing its own access, we separate this responsibility.

Figure 1 The main actors in an OAuth 2 system

The user will be able to log in using the authentication capabilities provided by the authorization server, and then use any of the apps integrated with that authorization server. In this article, we’ll implement the authorization server functionality using the new Spring Security authorization server framework.

2. Building a minimal authorization server

It’s time to build our authorization server using the new Spring Security authorization server framework. The first thing to do is make sure you have the dependencies for your project.

Listing 1 presents the pom.xml file of the Spring Boot project we’ll build for our demonstration. You observe that we only need to add the web dependency (since it will be a web app) and the Spring Security authorization server dependency for the authorization server framework.

Listing 1 The needed dependencies in your pom.xml file

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-authorization-server</artifactId>
        <version>1.0.0-M2</version>
    </dependency>
</dependencies>

Next thing we need to configure is the authorization server filter in the security filter chain. You can start from a minimal configuration provided by the utility method applyDefaultSecurity(HttpSecurity http) in the OAuth2AuthorizationServerConfiguration class. To apply several customizations, you use the getConfigurer() method as presented in listing 2.

Also, in listing 1, you can observe the use of authenticationEntryPoint() to make sure that any request that wasn’t authenticated will be correctly redirected to the login page provided by the server.

Listing 2 Configuration at security filter chain level

@Configuration
public class SecurityConfig {
    @Bean
    @Order(1)
    public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
        return http.build();
    }
}

Since we plan to use the authorization code grant type, the authorization server has to provide a way for the user to log in. For this reason, we’ll configure a second filter in the filter chain to set up the form login authentication on our server.

Definitely, you can implement any authentication method, but for our example, the simple way is to configure the form login with its convention configurations as provided by Spring Boot.

Listing 3 presents the minimum setup for enabling the form login authentication.

Listing 3 Configuration of form login support

@Bean
@Order(2)
public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception {
		
   http.authorizeHttpRequests(
       (authorize) -> authorize
               .anyRequest().authenticated()
   ).formLogin(Customizer.withDefaults());

   return http.build();
}

To make things easier to read and understand, in listing 4 I added both filters we previously defined in this article.

Listing 4 Full filter chain configuration

@Configuration
public class SecurityConfig {

  @Bean
  @Order(1)
  public SecurityFilterChain asSecurityFilterChain(HttpSecurity http) throws Exception {

    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
	
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
         .exceptionHandling(
            (exceptions) -> exceptions
                .authenticationEntryPoint(
                    new LoginUrlAuthenticationEntryPoint("/login"))
    )

    return http.build();
  }


  @Bean
  @Order(2)
  public SecurityFilterChain appSecurityFilterChain(HttpSecurity http) throws Exception {
		
     http.authorizeHttpRequests(
        (authorize) -> authorize.anyRequest().authenticated()
     ).formLogin(Customizer.withDefaults());

     return http.build();
  }

}

Being an authorization server dedicated to support grant types related to user authentication, it needs to also manage the user details. Fortunately, the user details management is made the same way as we’d do with any usual Spring Security application: with a UserDetailsService implementation.

Listing 5 demonstrates a simple InMemoryUserDetailsManager plugged in as a bean in the Spring context to take care of the user management. To simplify our example, I only added one user in-memory. The PasswordEncoder may also be added separately as a bean in case you want to provide specific implementations for the password encrypt/hash.

Listing 5 User management

@Bean
public UserDetailsService userDetailsService() {
  UserDetails userDetails = 
    User.withDefaultPasswordEncoder()
		.username("user")
		.password("password")
		.roles("USER")
		.build();

   return new InMemoryUserDetailsManager(userDetails);
}

The authorization server also needs to manage all the details about clients (apps that request tokens and use them to access the resources protected by the resource server). To define the client management, one needs to provide an implementation of the RegisteredClientRepository interface. The object model that represents a client known by the authorization server is defined by the RegisteredClient type. Some of the details you have to define are:

  • the client credentials (client ID and secret)
  • the authentication method
  • the supported grant types for each client
  • the redirect URIs (in case of authorization code grant type)
  • the scopes

Additionally, one can also specify per client:

  • token settings (for example the expiration time)
  • specific client settings (for example, whether it requires or not a consent)

Listing 6 shows how client management can be defined. In this simple example, I used an in-memory implementation of the RegisteredClientRepository contract. However, you can provide any implementation of the RegisteredClientRepository interface. For example, you might want to store and get them from a database.

Listing 6 Client management

@Bean
public RegisteredClientRepository registeredClientRepository() {
  RegisteredClient c =  
    RegisteredClient.withId(UUID.randomUUID().toString())
	  .clientId("client")
	  .clientSecret("{noop}secret")
	  .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
	  .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
	  .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
	  .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
	  .redirectUri("http://127.0.0.1:8080/authorized")
	  .scope(OidcScopes.OPENID)
	  .scope(OidcScopes.PROFILE)				
          .clientSettings(ClientSettings.builder()
                                .requireAuthorizationConsent(true).build())
	  .build();

	  return new InMemoryRegisteredClientRepository(c);
}

In most cases, you’d use a non-opaque token. JSON Web Token (JWT) is the most known and used implementation of a non-opaque token.

The authorization server framework supports both opaque and non-opaque tokens, but in this example I chose to use a non-opaque JWT implementation.

If you choose to use a JWT, you need to provide key pairs to sign the tokens. The JWKSource is the bean you need to plug into the authorization server configuration to tell it what key pairs can it use to sign/validate the JWTs. Observe that in this example, I used javax.security capabilities provided directly by the JDK to generate the key pair at the app startup. Eventually, in a real-world app, you might want to load the keys from a vault or use a different practice to manage them. Whichever your approach would be, as long as you end up with a JWKSource component managing these keys, the authorization server will know how to use them.

Listing 7 shows the definition of a JWKSource that manages a set containing only one key pair. In a real-world app, you might want to have multiple key pairs and eventually use key rotation for additional safety.

Listing 7 Private public key-pairs management

@Bean
public JWKSource<SecurityContext> jwkSource() {
  KeyPair keyPair = generateRsaKey();
  RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
  RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
  RSAKey rsaKey = new RSAKey.Builder(publicKey)
		.privateKey(privateKey)
		.keyID(UUID.randomUUID().toString())
		.build();
  JWKSet jwkSet = new JWKSet(rsaKey);
  return new ImmutableJWKSet<>(jwkSet);
}

private static KeyPair generateRsaKey() {
  KeyPair keyPair;

  try {
   KeyPairGenerator keyPairGenerator = 
      KeyPairGenerator.getInstance("RSA");

   keyPairGenerator.initialize(2048);
   keyPair = keyPairGenerator.generateKeyPair();

   } catch (Exception ex) {
    throw new IllegalStateException(ex);
   }

   return keyPair;
}

The last piece of the needed configuration is the AuthorizationServerSettings bean, which provides the main server configurations (for example, the endpoints that can be called by the client). If you wish to use the defaults, you can simply build an instance and plug it into the Spring context, as presented in listing 8.

Listing 8 General server configurations

@Bean
public AuthorizationServerSettings authorizationServerSettings() {
  return AuthorizationServerSettings.builder().build();
}

Now you have a minimal working authorization server. You can start the app to test it.

3. Testing the whole setup

You have a variety of things to test, but to prove the minimum, let’s just execute a full authorization code with PKCE grant type. We’ll act like the client and expect to go step by step through the flow and in the end get a JWT token.

Here are the steps we need to follow:

  1. Call the /authorize endpoint and expect to be redirected to the login page.
  2. Login with valid credentials (see the user we added with the UserDetailsService contract)
  3. After inserting correct credentials, we will be redirected to a page that doesn’t exist (the one we added as redirect URI) but we will get the authorization code as a request parameter in the URL.
  4. We take the authorization code, and we call the /token endpoint
  5. In the response, we get the access token (In the format of a JWT)

Figure 2 The authorization code grant type

Step 1. Call in browser the /authorize endpoint. The next code snippet gives you the full URL for test. http://localhost:8080/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8080/authorized&code_challenge=QYPAZ5NU8yvtlQ9erXrUYR-T5AGCjCF47vN-KsaI2A8&code_challenge_method=S256

Figure 3 The login screen

After logging in and being redirected (steps 2 and 3) you get the authorization code.

Figure 4 The page you are redirect back to with the authorization code provided

Step 4, you call the /token endpoint. The next code snippet provides you the full URL for calling the /token endpoint.

http://localhost:8080/oauth2/token?client_id=messaging-client&redirect_uri=http://127.0.0.1:8080/authorized&grant_type=authorization_code&code=Vw30RxdqfgdMw0xcWPV8tdxUH1nJdjny7tN5ub8td2dUrioCFXLnqvlgcFmkpbuwiviscp-wCvlxygjCn6pNEWYgo8tWlmkZg8LNgAY7lxTKZBuae7kabyX_0Tk_7ges&code_verifier=qPsH306-ZDDaOE8DFzVn05TkN3ZZoVmI_6x4LsVglQI

Figure 5 Calling the /token endpoint

Figure 6 Using HTTP Basic authentication to call the /token endpoint

Finally, you got a response with a JWT token. You can inspect your JWT token using jwt.io The next snippet shows an example of how the JWT might look like.

eyJraWQiOiIyY2E1MTU1ZS1iMzNkLTQzYWYtYWRmMy0yYWY4MmUzZTc4ODMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoiY2xpZW50IiwibmJmIjoxNjY4MTU0NjI1LCJzY29wZSI6WyJvcGVuaWQiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNjY4MTU0OTI1LCJpYXQiOjE2NjgxNTQ2MjV9.EUD5aqO0yKHa7k4wKMkl85aE_LmbcfKrLWZC9jt_-pfui5QOyOgmhQAuWQksEazpT09zajagPOtzwSelDNynPDCj_rpciw8IqVZp4ePoYc2lhv95o9qheC5m2qE3wD-6lYDey_pFckmEaGMuH9tp6btqetAyvvgJDaLGH9MGZhTcHFQi0ij93kUz3dxr8JX15eO_RfoMlWxf3rKrMt9B7aQBqu8ijGIRqvzVPqVhxep4Qjff8n0v9UK6v1zuPJtyJ8VvT4_y8iRz5uxC2scATTk6HywtdcZMix3ShNKqNSssnfZm5IhnjfT31lOl7YQWFLiyYbMSNinifnp3kMaiBw

Figure 7 Decoding the JWT in jwt.io

4. Summary

  • The Spring authorization framework is a Spring ecosystem project that enables you to easily build and customize an OAuth2/OpenID Connect server
  • The Spring authorization framework is not a product such as Keycloak (or similar third parties). You use the Spring authorization server framework to build your own authorization server, not just configure one.
  • Building a minimal authorization server is simple. The process includes the definition of a few base components among which we can enumerate: user management, client management, key pair management for token signing, and generic server configuration.
  • The framework offers very easy-to-customize ways, allowing you to easily and fast build an OAuth 2/OpenID Connect server.

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén