DEV Community

Nermin Karapandzic
Nermin Karapandzic

Posted on

Spring security new Authorization server (0.3.1) - part 2

In the previous part we've set up the minimum authorization server with in memory users and clients. In this part we will change the implementation slightly and make users not be stored in memory but instead in database.

Let's start with the entity

@Entity
@Getter
@Setter
@NoArgsConstructor
public class AppUser implements UserDetails {

  @Id
  private String id;
  private String username;
  private String password;
  @ManyToMany(fetch = FetchType.EAGER)
  @JoinTable(name = "user_authority",
      joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
      inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
  private Collection<Authority> authorities;
  private Boolean isAccountExpired = false;
  private Boolean isAccountLocked = false;
  private Boolean isCredentialsExpired = false;
  private Boolean isEnabled = true;

  public AppUser(String username, String password,
      Collection<Authority> authorities) {
    this.id = UUID.randomUUID().toString();
    this.username = username;
    this.password = password;
    this.authorities = authorities;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return this.authorities;
  }

  @Override
  public String getPassword() {
    return this.password;
  }

  @Override
  public String getUsername() {
    return this.username;
  }

  @Override
  public boolean isAccountNonExpired() {
    return !this.isAccountExpired;
  }

  @Override
  public boolean isAccountNonLocked() {
    return !this.isAccountLocked;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return !this.isCredentialsExpired;
  }

  @Override
  public boolean isEnabled() {
    return this.isEnabled;
  }
}
Enter fullscreen mode Exit fullscreen mode

We will use the ProviderManager with DaoAuthenticationProvider from Spring security, hence we implement the UserDetails in the entity.

You can also notice the @ManyToMany relationship to authorities.
Let's see the Authority entity:

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Authority implements GrantedAuthority {

  @Id
  private String id;
  private String name;

  public Authority(String name) {
    this.id = UUID.randomUUID().toString();
    this.name = name;
  }


  @Override
  public String getAuthority() {
    return this.name;
  }
}
Enter fullscreen mode Exit fullscreen mode

There's not much here, authority only has a name and an id. We had to implement the GrantedAuthority because if you remember in the UserDetails authorities is a Collection<? extends GrantedAuthority>. Hibernate will also create a join table based on the @JoinTable annotation in the AppUser entity.

The configuration for the default security config will have to change from the previous part, let's see what it looks like now:

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

  private final UserService userService;

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

    http
        .authorizeRequests()
        .antMatchers("/users/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().ignoringAntMatchers("/users/**")
        .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;
  }

}
Enter fullscreen mode Exit fullscreen mode

First, I allowed all requests to /users/** pattern, because I need a way to create a user. I only have an endpoint to create a new user, but in a real case scenario you probably would not allow all requests like this. After that I also need to disable csrf since I imagine you will want to call this endpoint from an spa. If you may want to create a register page as part of the spring app like the login page then you should leave csrf.

Next we expose an AuthenticationManager bean where we return a new ProviderManager and pass only a DaoAuthenticationProvider as it's provider. Before that we also instruct DaoAuthenticationProvider to use our implementation of UserDetailsService, which we injected before, and here is the implementation:

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

  private final AppUserRepository appUserRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return this.appUserRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));
  }

  public UserResponse save(CreateUserRequest request) {
    var user = new AppUser(request.getUsername(), request.getPassword(), List.of());
    this.appUserRepository.save(user);
    return new UserResponse(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Also you would probably not use the NoOpPasswordEncoder, but for simplicity sake I used it here. You can simply use any other PasswordEncoder there, you will likely also want to expose it as bean, and then when creating the user, before saving you would encode the password.

Now let's test the changes. Same as in the previous part, I will open the following link:
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

You should create your own code_challenge and verifier and replace them before opening the link.

I will be redirected to the /login page, but now I can't login yet because I don't have any users in the database. First let's create a user:

Image description

You should not return a password in the response like this, it's just for demonstration purposes.

Now let's use the username and password to login, you should get redirected to 'spring.io?code=...', copy the code and call the token endpoint:

Image description

Again, make sure you put your own code_verifier and code you got from the previous redirect.

Now we have users in the database, but our clients are still stored in memory. In the next part we will do the same thing but for the clients.

Top comments (0)