In my earlier posts, I described how to do authentication and authorization for a custom database schema - with Spring Security and PicketLink.
In this post, I use the same custom schema to show how to use JWT with Spring Boot.
Most of these are covered in https://medium.com/wolox/securing-applications-with-jwt-spring-boot-da24d3d98f83
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();
}
...
}
We do the configuring for authentication manager, password encoder all in the class WebSecurityConfig (the one annotated with @EnableWebSecurity).
As we are building this for HTTP RESTFul services, we need to have an endpoint to which users will make a REST call and get back a JWT token. And then, we will need to have a filter which will intercept all REST calls and check if the token is valid or not.
So, for the first part, we can simply write a filter which extends org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.
We need to override two methods: attemptAuthentication, successfulAuthentication
The first method is called when authenticating the user. But, as explained above, we are authenticating via an endpoint. So, what is the endpoint?
As we're extending the Spring's internal UsernamePasswordAuthenticationFilter, by default, Spring sets the endpoint as '/login'. We can change this endpoint to something else with a call to (in the constructor):
super.setFilterProcessesUrl("/authenticate");
So, here, I have set the auth endpoint to 'authenticate'. When the user hits the endpoint, as a POST request with body like:
{ "username": "user1", "password": "pwd1"}
Spring Spring calls our attemptAuthentication method. We need to first read the body from the request (JSON parse). And then we call the getAuthenticationManager().authenticate. This will in turn call our authentication manager (which we configured earlier) which uses our UserService (which makes JPA calls to our custom database to load the user along with the password hash).
If the auth is successful, Spring calls our successfulAuthentication method. It is in this method where we need to create the JWT token.
We're making use of the jjwt library to create the JWT token. We set the login name as the subject in the token. We are also setting the role that the user is assigned to in the claims part. This role information is needed and useful for the caller. We're also setting the expiration time for the token.
We are reading the key hash needed for the JWT token from the properties file and are using it. Likewise, the expiration time is also read from properties file (set in minutes). In the response header, we set the token. Once the user gets back the token, they can use it in subsequent requests. This has to be set in the request header 'Authorization' with the format 'Bearer <token>'.
For subsequent requests, we need to write a filter which should check the token validity. Allowing/disallowing an URL and also allowing specific calls for specific roles are all set in the usual manner in the WebSecurityConfig (this is standard Spring Security stuff).
For URL based authorization, we can set it up in the security config,such as:
.antMatchers("/unsecured/*").permitAll()
.antMatchers("/general/*").authenticated()
.antMatchers("/normal/*").hasRole(AppRole.USER.toString())
.antMatchers("/admin/*").hasRole(AppRole.ADMIN.toString())
I have named the filter for token verification as, well, TokenVerificationFilter. This class extends the Spring org.springframework.security.web.authentication.www.BasicAuthenticationFilter class which extends the org.springframework.web.filter.OncePerRequestFilter.
We need to override the doFilterInternal method. We read the token from the header here and call the parseClaims method of the jjwt library. This call verifies the signature and also checks if the token has expired (by checking the expiration time). If the token is valid, we set the user details in the Spring security context, like:
SecurityContextHolder.getContext().setAuthentication(authentication);
We set these two filters in the WebSecurityConfig such as:
.addFilter(new AuthenticationFilter(keyHash, expMinutes, authenticationManager()))
.addFilter(new TokenVerificationFilter(keyHash, authenticationManager()))
The complete source code for this post is available in my
github repoReferences:
https://www.toptal.com/spring/spring-security-tutorial
https://medium.com/wolox/securing-applications-with-jwt-spring-boot-da24d3d98f83