DEV Community

Nermin Karapandzic
Nermin Karapandzic

Posted on

Spring security new Authorization server (0.3.1) - part 1

As you may already know, some time ago spring security marked the old spring-security-oauth2 project as deprecated and they started implementing the features it supported to the core spring security.

After some time most of the things were implemented in the core, except the authorization server. So for some time now we had the ability to use spring security's resource-server, client and login but we had no way of building an authorization server, unless of course we used the deprecated project.

In the meantime spring security team was working on the new implementation of the authorization which is now production ready but still in early stages, currently version 0.3.1.

In this post I plan to cover the basics on how to setup the minimum for the authorization server to work and in the following parts I will also upgrade it to not use the in memory clients and users and also then use it together with the resource server and client from the core spring security library.

At the moment of writing this it seems like the documentation is also in the early stages, although I wouldn't expect too much from the documentation later on either if it was to be judged by the docs for the rest of the spring security.
You can find the official docs here.

To get started, create a new spring or spring-boot application and add the following dependency:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.3.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

To get started we need to add some configurations. One class will be responsible for configuring oauth2 part, and the other for the usual spring security config.

@Configuration
public class Oauth2Config {

  @Bean
  @Order(HIGHEST_PRECEDENCE)
  public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
      throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

    http
        // Redirect to the login page when not authenticated from the
        // authorization endpoint
        .exceptionHandling((exceptions) -> exceptions
            .authenticationEntryPoint(
                new LoginUrlAuthenticationEntryPoint("/login"))
        );

    return http.build();
  }

  @Bean
  public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("client")
        .clientSecret("{noop}secret")
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
        .redirectUri("http://spring.io")
        .scope(OidcScopes.OPENID)
        .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
  }

  @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;
  }

  @Bean
  public ProviderSettings providerSettings() {
    return ProviderSettings.builder().build();
  }

}
Enter fullscreen mode Exit fullscreen mode

Here we expose a SecurityFilterChain which is a new way of configuring spring security instead of the old where we would extend the WebSecurityConfigurerAdapter...
Then we use the static method of OAuth2AuthorizationServerConfiguration to applyDefaultSecurity. We have many options for configuration here but for now we will stick to the defaults. Also this project is not yet compliant with spring boot and you will probably not need to define this in the future and defaults will be used by default.

After this we configure the exception handling for the Oauth2 filter chain, and we define the '/login' as the authentication entry point. When we use the authorization endpoint ("oauth2/authorize?...) and we are not already authorized we will be redirected to /login.

Next we expose a RegisteredClientRepository bean, which you can think of as a UserDetailsService but only for clients. If you check the interface you will see it has the same methods as the UserDetailsService.

We then create one in memory client to use in our example and return a new InMemoryRegisteredClientRepository.

We also need to define a key source which the library needs to sign and verify tokens. We will add one key pair to the source and expose the bean.

And finally we need to expose the ProviderSettings bean which defines the endpoints settings for the provider, defaults are used if not provided. You can see the defaults here, or later in the .well-known endpoint.

That's one configuration class done, now let's configure the users.

@Configuration
public class SecurityConfig {

  @Bean
  public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
      throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        // Form login handles the redirect to the login page from the
        // authorization server filter chain
        .formLogin(Customizer.withDefaults());

    return http.build();
  }

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

    return new InMemoryUserDetailsManager(userDetails);
  }

}
Enter fullscreen mode Exit fullscreen mode

There's not much to say here, we define the UserDetailsServiceimplementation, expose it as Bean and setup some basic configuration. Later we will change this in memory implementation with real database implementation, but for now we work with in memory users and clients.

If you start the application now, and visit .well-known/openid-configuration you should see response like this:

Well known configuration response

You can read more on what is this endpoint and why it's needed here.

Now we can test the server. Note that we defined the client with
ClientAuthenticationMethod.NONE and that is because I will use postman to get the token, postman is a public client hence we cannot safely send client credentials and for that reason we use PKCE. It seems like spring security authorization server will require PKCE by default if the client authentication is set to none, which is a good thing.

You can read more about PKCE here.

Note also we're using the authorization_code grant_type which is the most common, and I won't discuss the other grant types in this part.

Before testing, you will need a code_challenge and verifier in order to make PKCE work. You would usually create these on your client, but for the purposes of testing you can use this tool.

So the verifier is a random string that we generate and the code-challenge is that string hashed. In the authorization call we send the code-challenge as well as the method used for hashing.
Then when we get back the code and we need to go get the token with that code, we will now provide the verifier so the server can be sure that we are the ones who asked for the code.

Even if the first request for the code get's intercepted because hash is not reversible the attacker couldn't know what is the verifier, and he cannot get the token without the verifier, but we can because we have the verifier.

Now open:
http://localhost:8080/oauth2/authorize?response_type=code&client_id=client&scope=openid&redirect_uri=http://spring.io&code_challenge=YHRCg0i58poWtvPg_xiSHFBqCahjxCqTyUbhYTAk5ME&code_challenge_method=S256

but change the code_challenge to the challenge you generated.
We set the redirect_uri to the spring.io but it is usually going to be your client app, since we don't have one it doesn't matter, we will get redirected to spring.io and copy the code from the request parameters directly.

When you open the page, you will be redirected to /login, now use the user you defined in the UserDetailsService and login, after that you will get redirected to 'spring.io?code=...'

code response

Now when we have the code, we can go get the tokens.

token response

To get the token we use the /oauth2/token endpoint, and pass the parameters:
grant_type: authorization_code
code: code you got in the previous redirect
client_id: id of the registered client
code_verifier: random generated string to verify the hash sent in first request
redirect_url: must be the registered redirect_uri

And that's it. In the next chapters we will use database to store the users and clients and we will create a resource server to see how it works together.

Top comments (1)

Collapse
 
uniquejava profile image
cyper

You explained very well, thank you very much.