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;
}
}
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;
}
}
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;
}
}
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);
}
}
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:
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:
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)