In my earlier post, I described how to do authentication and authorization with PicketLink using a custom schema. In this post, I use the same schema to perform the same with Spring Security and Spring Data JPA. Spring Security provides everything we need and we can configure authentication and authorization supported from a database using queries. In this post, I show an example of how to achieve the same using Spring Data JPA repositories.
Most of these are covered in the post: http://www.baeldung.com/spring-security-authentication-with-a-database.
I build on top of this by tweaking it to use the same schema as used in my earlier post where the password is kept in a separate table:
A simple one, with a table to store the user info, a separate related table to store password (this is useful because, when bringing back user from DB, we don't need to bring back the password related info). A master table for role and another table to associate the roles for a user.
I write corresponding JPA entities for these tables. Importantly, I don't have a relation from AppUser to UserPassword entity (it's the other way around) - simply because, as mentioned above, I don't want to bring back password information when I load user. Same applies in case of roles too. One of the basic concepts to remember about security is that, information should not be provided unless and until it is asked for (strictly on a need-to-know basis).
So, how do we encrypt the password and store the same in DB? Spring provides excellent support for encoders. So, we declare a bean in the config file, such as:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Most of these are covered in the post: http://www.baeldung.com/spring-security-authentication-with-a-database.
I build on top of this by tweaking it to use the same schema as used in my earlier post where the password is kept in a separate table:
A simple one, with a table to store the user info, a separate related table to store password (this is useful because, when bringing back user from DB, we don't need to bring back the password related info). A master table for role and another table to associate the roles for a user.
I write corresponding JPA entities for these tables. Importantly, I don't have a relation from AppUser to UserPassword entity (it's the other way around) - simply because, as mentioned above, I don't want to bring back password information when I load user. Same applies in case of roles too. One of the basic concepts to remember about security is that, information should not be provided unless and until it is asked for (strictly on a need-to-know basis).
So, how do we encrypt the password and store the same in DB? Spring provides excellent support for encoders. So, we declare a bean in the config file, such as:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Let us now look at how to create an user with password and associate a role. We can create the entities and persist with Spring Data JPA repository:
String role="ADMIN";
AppUser appUser = new AppUser();
appUser.setLoginName(email);
...
appUserRepository.save(appUser);
UserRole userRole = new UserRole();
userRole.setAppUser(appUser);
userRole.setRole(roleMasterRepository.findByName(role));
userRoleRepository.save(userRole);
UserPassword userPassword = new UserPassword();
userPassword.setAppUser(appUser);
userPassword.setPasswordHash(passwordEncoder.encode(password));
userPasswordRepository.save(userPassword);
The above block of code can be placed within a transaction and we can call this service from wherever user creation needs to be done.
For the user authentication to work, we need to wire a authentication provider which can be a DaoAuthenticationProvider as we are storing the info in a database. We need to pass an service to this provider which implements org.springframework.security.core.userdetails.UserDetailsService.
We also need to set the password encoder which is same as we saw above. The UserDetailsService is an interface, so we need to override the method loadUserByUsername() which should return a org.springframework.security.core.userdetails.UserDetails instance.
So, we create a class which implements UserDetails interface. I have named this as UserLoginDto and I pass the AppUser object and the role of the user. Using this the overridden methods can return corresponding values. For example, the method isEnabled() can simply return appUser.isActive().
Most importantly, we have to override the getAuthorities() method which needs to return the list of roles assigned for the user. Since we have retrieved the roles for the user from the DB, we can use that to build the Set and return it
(note: I have used only only role per user in this example, we can easily expand on that).
public class UserLoginDto implements UserDetails {
public UserLoginDto(AppUser appUser, String passwordHash, AppRole appRole) {
this.appUser = appUser;
...
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<RoleGrantedAuthority> roleSet = new HashSet<>();
roleSet.add(new RoleGrantedAuthority("ROLE_"+ appRole.toString()));
return roleSet;
}
@Override
public boolean isEnabled() {
return appUser.isActive();
}
...
}
So, when Spring is going to authenticate, it will call the loadUserByUsername on the service. We use Spring Data JPA repositories of AppUser, UserPassword and UserRole to fetch these values from the DB. We then build the UserLoginDto object and return it from the method.
Note that, we need to fetch the password at this place as it is needed to authenticate the user. However, the AppUser object is free of this password and we can pass this around back to the UI separately. This should be enough for authentication.
For URL based authorization, we can set it up in the security config,such as:
.antMatchers("/userpages/**").hasAnyRole(AppRole.USER.toString(), AppRole.ADMIN.toString())
.antMatchers("/adminpages/**").hasRole(AppRole.ADMIN.toString())
The complete source code for this post is available in my github repo.