Using JWT Keys In Spring Security
--
When you want to access an API, you need an Authentication mechanism. You can use username, and password authentication, which is not very safe. You can use JWT keys, which are easy to implement, and are very standard these days.
I’m not going to add every code here. if you want to copy it, the code may not run. You can download the full code from here https://github.com/dogukanhan/spring-jwt-example. If the code in the repository doesn’t run, please open an issue.
Let’s start with creating our Spring Boot project. https://start.spring.io/
Spring Web and Spring Security dependencies are enough for this example. I’m going to keep this writing simple.
You must add the dependencies below to use JWT. Make sure you are using the latest versions for your project.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
The application has two roles, Admin and User. I also handled if the user did not has a role. I also added a role prefix.
public interface ApiRole {
String ROLE_PREFIX = "ROLE_";
String ROLE_ADMIN = "ADMIN";
String ROLE_USER = "USER";
}
The simple controller is our test controller. We will see if things are working correctly or not. @RollesAllowed
is for checking if the user who makes the request is allowed to do the operation.
@RestController
public class SimpleController {
@RolesAllowed(ApiRole.ROLE_ADMIN)
@GetMapping("/admin")
public String adminAction(){
return "only users that has Admin role can see this";
}
@RolesAllowed(ApiRole.ROLE_USER)
@GetMapping("/user")
public String userAction(){
return "only users that has User role can see this";
}
@GetMapping("/no-role")
public String noRoleAction(){
return "every authenticated user can see this";
}
}
Spring Security uses the UserDetails interface for authentication and authorization. I created a class which implements this.
public class ApiUser implements UserDetails {
private String password;
private String username;
private Collection<GrantedAuthority> authorities;
public ApiUser(String username, String password, Collection<GrantedAuthority> authorities) {
this.password = password;
this.username = username;
this.authorities = authorities;
}
public ApiUser(String username, Collection<String> authorities) {
this.username = username;
this.authorities = authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
We store users’ roles in the authorities
whose type is a collection of GrantedAuthority
.
Authorities stored in the JWT token are strings, so we have to convert them to the relevant class.
authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
We need a user repository for retrieving users. I don’t want things to get complicated. So, I only used the in-memory store. I created three users. One is admin, one is user and the last one has no role.
@Component
public class UserRepo {
private static final List<ApiUser> users = List.of(
new ApiUser("adminuser","test", List.of(new SimpleGrantedAuthority(ApiRole.ROLE_PREFIX + ApiRole.ROLE_ADMIN),
new SimpleGrantedAuthority(ApiRole.ROLE_PREFIX + ApiRole.ROLE_USER))),
new ApiUser("user","test", List.of(new SimpleGrantedAuthority(ApiRole.ROLE_PREFIX + ApiRole.ROLE_USER))),
new ApiUser("noroleuser","test", List.of())
);
public Optional<ApiUser> findByUsernameAndPassword(String username, String password) {
return users.stream().filter(x->x.getUsername().equals(username) && x.getPassword().equals(password))
.findFirst();
}
}
We need a JWT key generator and parser. We send a Key when the user sends an authentication request. In every request except authentication requests, we check if the token which is in the request’s header is valid. generateJwtToken
generates a token with given user details. getUserDetailsFromToken
validates and returns the UserDetails from the token.
public String generateJwtToken(UserDetails userDetail) {
Map<String, Object> claims = new HashMap<>();
claims.put("Authorities", userDetail.getAuthorities());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetail.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(
new Date(System.currentTimeMillis() + 3600 * 1000))
.signWith(SignatureAlgorithm.HS512,
jwtSecret).compact();
}
I put the user’s authorities in the Authorities key in the JWT. We have to use the same key when we’re deserializing. Don’t forget to change the expiration time depending on your application.
Set your jwt secret in the application.properties file.
jwt.secret=testjwtsecret
In getUserDetialsFromToken
we check if the token is valid. Then we parse the JWT key to retrieve the username and the claims.
public Optional<UserDetails> getUserDetailsFromToken(String token) {
final Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token).getBody();
boolean valid = !claims.getExpiration().before(new Date());
if (!valid) {
return Optional.empty();
}
var username = claims.getSubject();
Collection<LinkedHashMap<String, String>> claimMap =
claims.get("Authorities", Collection.class);
Collection<String> authorities =
claimMap
.stream()
.map(x -> x.values()
.stream()
.findFirst()
.orElse("")).collect(Collectors.toList());
return Optional.of(new ApiUser(username, authorities));
}
To check if the token is valid for every request, we have to add a request filter. Request filters have a doFilterInternal method that we should use for our case.
Firstly, we have to check if the request has the JWT key.
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (isEmpty(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
Second, we must check if the JWT key is valid and contains the user’s information.
final String token = header.split(" ")[1].trim();
var userDetailsOptional = jwtTokenUtil.getUserDetailsFromToken(token);
if (userDetailsOptional.isEmpty()) {
chain.doFilter(request, response);
return;
}
Lastly, we have to add the UserDetails to the context. So, other filters or methods can access user data.
var userDetails = userDetailsOptional.get();
UsernamePasswordAuthenticationToken
authentication = new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
After adding a bunch of things, we have to configure Spring Security. SecurityFilterChain bean is the way of configuration.
Let’s disable CORS and CSRF first. We don’t need them right now.
http = http.cors().and().csrf().disable();
We’re using JWT tokens, so we don’t need sessions.
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
Add exception handling for not authorized users.
http = http
.exceptionHandling()
.authenticationEntryPoint(
(request, response, ex) -> {
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
);
}
)
.and();
We must permit all requests to the auth endpoint. Other paths are dependent on your implementation.
http.authorizeRequests()
.antMatchers("/auth/authenticate").permitAll()
.anyRequest().authenticated();
The last step is adding the JWT filter to the filters. Filters run in order. They can change the request or apply rules depending on the request. If you don’t put the filters in the correct order, it won’t work. The addFilterBefore
method adds a filter before any given filter. The method helps you to put the JWT filter into the correct position.
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
Let’s add an authentication controller so users can get tokens. I added another method to see what the token has inside.
@PostMapping("/auth/authenticate")
public String Authenticate(@RequestBody AuthRequest request){
var userDetail = this.userRepo
.findByUsernameAndPassword(
request.getUsername(),
request.getPassword())
.orElseThrow();
var token = tokenUtil.generateJwtToken(userDetail);
return token;
}
@GetMapping("/auth/me")
public UserDetails Me(){
var detail = (UserDetails) SecurityContextHolder
.getContext().getAuthentication().getPrincipal();
return detail;
}
We can test it now.
Send a request to http://localhost:8080/auth/authenticate and get a JWT token.
Set the request’s header with the JWT key and see if http://localhost:8080/auth/me works.
Test it if you can see http://localhost:8080/admin
You can test with other users given in the API repository with different roles.
Thanks for reading. You can download the code here https://github.com/dogukanhan/spring-jwt-example
Resources
https://www.toptal.com/spring/spring-security-tutorial
https://www.baeldung.com/spring-security-oauth-jwt
https://www.javainuse.com/spring/boot-jwt