Spring security new Authorization server (0.3.1) - part 3
Tags: java, security, oauth2
Published: 2022-09-28 10:33:35

In this part we will continue building the server and specifically we will change the in memory client to a database, persistent solution.

First let's start with the entities, you can inspect the `RegisteredClient` class provided by the library which is used by `RegisteredClientRepository` to get an idea how we need to model our entity.

Here are the fields on the `RegisteredClient` class:
- id (String)
- clientId (String) can be the same field as id
- clientIdIssuedAt (Instant) - not included in our entity
- clientSecret (String)
- clientSecretExpiresAt (Instant) - Not included in our entity
- clientName (String) - Not included in our entity
- clientAuthenticationMethods (Set<ClientAuthenticationMethod>)
- authorizationGrantTypes (Set<AuthorizationGrantType>)
- redirectUris (Set<String>)
- scopes (Set<String>)
- clientSettings (ClientSettings) - Not included in our entity
- tokenSettings (TokenSettings) - Not included in our entity

Based on this you can model your client entity however you want but in the end you have to be able to construct a `RegisteredClient` from it. Since we don't have an interface here as we had for the `UserDetails`, we cannot simply implement the interface and say this entity is the `RegisteredClient`, but instead what I did is provide a factory class that builds a `RegisteredClient` from an entity.

Client entity:

```java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Client {

  @Id
  private String id;
  private String secret;
  @ElementCollection(fetch = FetchType.EAGER)
  private Set<AuthorizationGrantType> grantTypes;
  private ClientAuthenticationMethod authenticationMethod;
  @OneToMany(mappedBy = "client", fetch = FetchType.EAGER)
  private Set<ClientRedirectUrl> redirectUris;
  @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER)
  @JoinTable(name = "client_scope_mapping",
    joinColumns = @JoinColumn(name = "client_id", referencedColumnName = "id"),
    inverseJoinColumns = @JoinColumn(name = "scope_id", referencedColumnName = "id")
  )
  private Collection<ClientScope> scopes;

  public Client(CreateClientRequest request) {
    this.id = request.getId();
    this.secret = request.getSecret();
    this.grantTypes = request.getGrantTypes();
    this.authenticationMethod = request.getAuthenticationMethod();
  }

  public static RegisteredClient toRegisteredClient(Client client) {
    RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
      .clientId(client.getId())
      .clientSecret(client.getSecret())
      .clientAuthenticationMethod(client.getAuthenticationMethod())
      .authorizationGrantTypes(
          authorizationGrantTypes -> authorizationGrantTypes.addAll(client.getGrantTypes()))
      .redirectUris(
          redirectUris -> redirectUris.addAll(client.getRedirectUris()
              .stream()
              .map(ClientRedirectUrl::getUrl)
              .collect(Collectors.toSet())))
      .scopes(scopes -> scopes.addAll(client.getScopes()
          .stream()
          .map(ClientScope::getScope)
          .collect(Collectors.toSet())));
    return builder.build();
  }

}
```
You see I decided to store the grant types for each client as an `@ElementCollection` which will store the serialized set of `AuthorizationGrantType` in the database as `tinyblob`, you could choose to have a separate table to hold all available grant types, and then just join the ones you need for each client, you'd probably need a join table as well.

Then we have @OneToMany with redirectUris (`ClientRedirectUrl`) and @ManyToMany with scopes (`ClientScope`).

ClientRedirectUrl entity:

```java
@Entity
@Getter
@NoArgsConstructor
@Table(name = "client_redirect_uri")
public class ClientRedirectUrl {
  @Id
  private String id;
  private String url;
  @Setter
  @ManyToOne
  @JoinColumn(name = "client_id", referencedColumnName = "id")
  private Client client;

  public ClientRedirectUrl(String url, Client client) {
    this.id = RandomStringUtils.randomAlphanumeric(10);
    this.url = url;
    this.client = client;
  }
}
```

ClientScope entity:

```java
@Entity
@Getter
@NoArgsConstructor
@Setter
@Table(name = "client_scope")
public class ClientScope {

  @Id
  private String id;
  private String scope;

  public ClientScope(String scope) {
    this.id = RandomStringUtils.randomAlphanumeric(10);
    this.scope = scope;
  }
}
```
This is the final schema diagram:

![Schema diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iqb9gsrz4qxknppoz2d.png)

Next, in the Oauth2Config class, which we created in the first part, we need to remove the bean that provides an in memory client repository. Instead we will provide a @Service which will implement the `RegisteredClientRepository`.

ClientService:

```java
@Service
@RequiredArgsConstructor
public class ClientService implements RegisteredClientRepository {

  private final ClientRepository clientRepository;
  private final ClientRedirectUrlRepository clientRedirectUrlRepository;
  private final ClientScopeRepository clientScopeRepository;

  @Override
  public void save(RegisteredClient registeredClient) {
  }

  @Override
  public RegisteredClient findById(String id) {
    var client = this.clientRepository.findById(id).orElseThrow();
    return Client.toRegisteredClient(client);
  }

  @Override
  public RegisteredClient findByClientId(String clientId) {
    var client = this.clientRepository.findById(clientId).orElseThrow();
    return Client.toRegisteredClient(client);
  }

  public ClientResponse createClient(CreateClientRequest request) {
    var scopes = request.getScopes().stream().map(ClientScope::new).collect(Collectors.toSet());
    scopes.forEach(this.clientScopeRepository::save);

    var client = new Client(request);
    client.setScopes(scopes);
    this.clientRepository.save(client);

    client.setRedirectUris(request.getRedirectUris().stream()
        .map(url -> new ClientRedirectUrl(url, client))
        .collect(Collectors.toSet()));
    client.getRedirectUris().forEach(this.clientRedirectUrlRepository::save);

    return new ClientResponse(client);
  }
}
```

And now this is how the Oauth2Config class looks like:

```java
@Configuration
@RequiredArgsConstructor
public class Oauth2Config {

  private final ClientService clientService;

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

}
```
I also created and endpoint for creating a new client, and had to also allow requests to it in the default config, so it now looks like this:

```java
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

  private final UserService userService;

  @Bean
  public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
      throws Exception {

    http
        .authorizeRequests()
        .antMatchers("/users/**").permitAll()
        .antMatchers("/clients/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().ignoringAntMatchers("/users/**", "/clients/**")
        .and()
        .formLogin(Customizer.withDefaults());

    return http.build();
  }

  @Bean
  public AuthenticationManager authenticationManagerBean() throws Exception {
    var provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userService);
    provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); //temporary
    return new ProviderManager(provider);
  }

  @Bean
  public UserDetailsService userDetailsService() {
    return this.userService;
  }

}
```
Maybe it's not a great idea to permitAll for the wildcard as in my case, but again you can modify this to your needs.

Let's test the authorization again, first create a client:

![Create client response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7jf99bknk0f03sxgyscq.png)

Then, create a user:

![Create user response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/avs1sdwi6vvjfufer7v7.png)

Then authorize,
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

use your own code_challenge, you will need a verifier.

Get the token:

![Token response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jz63a5bj4cy8htvtf8x9.png)

It works, there are some obvious things that should probably be changed like the authorization matchers configuration, or the password encoder or the lack of one, but you get the point. 

You can find the full code [here](https://github.com/NerminKarapandzic/authorization-server), but a fair warning it will probably change in the future as I will be looking to build an open source version out of it.