How we can implement step by step a stateless authentication using JWT to secure a REST API endpoints built with the help of Spring Boot and Spring Security.
In this tutorial, we will see how we can implement a stateless authentication using JWT to secure a REST API endpoints built with the help of Spring Boot and Spring Security. Are you ready to get started ?
Session Cookie Based Authentication:
When we talk about The session-based approach, it means that it's the server that is responsible for managing the authentication state of the user, and we have noticed that today, the most common usage of authentication is the session-based approach. In this approcah, a session id is generated by the server and stored in a cookie within the JSESSIONID paramter. This implies that the server stores the session key in itself thus once the server reboots or redirect requests to a different server using load balancers, your "state" of session key becomes useless.
Stateless authentication:
What Stateless authentication means is that we don’t want to store any information about user on the server. One of the reasons can be serving your application through a load balancer and we do not want to use sticky sessions. With this configuration in place the user can reach out different backend servers transparently without having the need to reauthenticate. For REST API also, this may sound a better approach because we don’t want to rely on Basic Auth.
The basic idea behind a stateless authentication is that the user authenticates against your application, if the operation is successful the server will respond with a token that the user should send on any request using an Http Header or a Cookie to prove his identity (for our article we will use Http Headers). On the other side the server also needs to validate our token and verify if still valid, not expired and nobody tampered with it.
Exactly this can be achieved by the use of JWT.
Requirements:
Note: This tutorial was built using Spring Boot 2.0.4.RELEASE.
Before we begin coding let’s first define our dependencies and what we will need to build our application. Above is our pom.xml with all needed dependencies.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
Configure Security:
After defining the dependencies, let’s know enable security by adding a Configuration class that extends WebSecurityConfigurerAdapter.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because we store token in header
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// allow anonymous resource requests
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers(
HttpMethod.OPTIONS
).permitAll()
.antMatchers("/api/open/**").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl().disable();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
For our application to be exposed to third parties clients it should support CORS, this is why we also need to create a class where we define our CORS configuration by defining a CorsFilter Bean.
@Configuration
public class RestConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:4200");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
How to go Stateless?
As you can see in our Class WebSecurityConfig.java
we have told Spring Security that it should not store any session information and to be stateless using the method sessionCreationPolicy().
We also disabled CSRF because we will store our token in the header not in an HttpOnly cookie.
Now let’s define our JwtAuthenticationTokenFilter, this filter intercept all user’s requests and look for the Token in Authorization request header, if it exist it will be checked for validity then decide to authorize the request or deny it.
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(this.getClass());
@Autowired
@Qualifier(value = "jwtUtilWithoutDbCheckImpl")
private JwtUtil jwtTokenUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.refresh.header}")
private String refreshTokenHeader;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.addHeader("Access-Control-Allow-Headers",
"Access-Control-Allow-Origin, Origin, Accept, X-Requested-With, Authorization, refreshauthorization, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Credentials");
if (response.getHeader("Access-Control-Allow-Origin") == null)
response.addHeader("Access-Control-Allow-Origin", "http://localhost:4200");
if(response.getHeader("Access-Control-Allow-Credentials") == null)
response.addHeader("Access-Control-Allow-Credentials", "true");
if(response.getHeader("Access-Control-Allow-Methods") == null)
response.addHeader("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, DELETE");
String token;
if(!request.getMethod().equals("OPTIONS")){
token = request.getHeader(this.tokenHeader).substring(6);
}else{
token = request.getHeader(this.tokenHeader);
}
if (token != null && !token.equals("")) {
if(jwtTokenUtil.isTokenExpired(token)){
response.setStatus(490);
return;
}
if (jwtTokenUtil.validateToken(token)) {
String username = jwtTokenUtil.getUsernameFromToken(token);
List autorities = jwtTokenUtil.getRolesFromToken(token);
logger.info("checking Validity of JWT for user ");
logger.info("checking authentication for user " + username);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, autorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
We also need to define the Class JwtAuthenticationEntryPoint that is invoked when a user tries to access a secure REST resource without supplying any credentials, in that particular case the server should respond with a 401 Unautorized message as there is no login page to redirect to.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secure REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Next create the JwtUser Class and JwtUserFactory Class.
public class JwtUser implements UserDetails {
private static final long serialVersionUID = 1L;
private final Long id;
private final String username;
private final String password;
private final Collection authorities;
private final boolean enabled;
public JwtUser(
Long id,
String username,
String password, Collection authorities,
boolean enabled
) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
this.enabled = enabled;
}
@JsonIgnore
public Long getId() {
return id;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public Collection getAuthorities() {
return authorities;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
public final class JwtUserFactory {
private JwtUserFactory() {
}
public static JwtUser create(User user) {
return new JwtUser(
user.getId(),
user.getUsername(),
user.getPassword(),
mapToGrantedAuthorities(new ArrayList(user.getRoles())),
user.isEnabled()
);
}
private static List mapToGrantedAuthorities(List authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority("ROLE_"+authority.getRoleName().toUpperCase()))
.collect(Collectors.toList());
}
}
Now define the utility Class JwtUtilWithoutDbCheckImpl that implements JwtUtil interface, so we can manage our Token easily from creation to validation.
@Component
public class JwtUtilWithoutDbCheckImpl implements JwtUtil, Serializable {
private static final long serialVersionUID = -3301605591108950415L;
static final String CLAIM_KEY_ID = "jti";
static final String CLAIM_KEY_USERID = "userId";
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_CREATED = "created";
static final String CLAIM_KEY_ROLES = "roles";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Autowired
private UserDetailsService userDetailsService;
@Override
public String getIdFromToken(String token) {
String id;
try {
final Claims claims = getClaimsFromToken(token);
id = claims.getId();
} catch (Exception e) {
id = null;
}
return id;
}
@Override
public Long getUserIdFromToken(String token){
Long userId;
try {
final Claims claims = getClaimsFromToken(token);
userId = Long.parseLong(claims.get(CLAIM_KEY_USERID).toString());
} catch (Exception e) {
userId = null;
}
return userId;
}
@Override
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
@SuppressWarnings("unchecked")
@Override
public List getRolesFromToken(String token) {
List roles;
try {
final Claims claims = getClaimsFromToken(token);
roles = (List)claims.get(CLAIM_KEY_ROLES);
} catch (Exception e) {
roles = null;
}
return roles != null ? roles.stream().map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList()) : null;
}
@Override
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
@Override
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
@Override
public Claims getClaimsFromToken(String token) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, IllegalArgumentException
{
Claims claims;
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims;
}
private Date generateExpirationDate(String type) {
if(type.equals("token")){
return new Date(System.currentTimeMillis() + expiration * 1000);
}else{
return new Date(System.currentTimeMillis() + expiration * 5 * 1000);
}
}
@Override
public Boolean isTokenExpired(String token) {
try{
getClaimsFromToken(token);
return false;
}catch(ExpiredJwtException ex){
return true;
}
}
@Override
public Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
@Override
public JwtAuthenticationResponse generateToken(String username) {
Map claims = generateClaims(username);
String token = doGenerateToken(claims);
String refreshToken = doGenerateRefreshToken(claims);
return new JwtAuthenticationResponse(token, refreshToken);
}
@Override
public JwtAuthenticationResponse refreshToken(String username) {
String newToken;
String newRefreshToken;
try {
final Map claims = generateClaims(username);
newToken = doGenerateToken(claims);
newRefreshToken = doGenerateRefreshToken(claims);
} catch (Exception e) {
newToken = null;
newRefreshToken = null;
}
return new JwtAuthenticationResponse(newToken, newRefreshToken);
}
private Map generateClaims(String username){
final JwtUser userDetails = (JwtUser)userDetailsService.loadUserByUsername(username);
Map claims = new HashMap<>();
claims.put(CLAIM_KEY_ID, UUID.randomUUID().toString());
claims.put(CLAIM_KEY_USERID, userDetails.getId());
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
claims.put(CLAIM_KEY_ROLES, AuthorityUtils.authorityListToSet(userDetails.getAuthorities()));
return claims;
}
private String doGenerateToken(Map claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate("token"))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
private String doGenerateRefreshToken(Map claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate("refresh"))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
@Override
public Boolean validateToken(String token) {
try{
getClaimsFromToken(token);
return true;
}catch(Exception ex){
return false;
}
}
}
public interface JwtUtil {
public String getIdFromToken(String token);
public Long getUserIdFromToken(String token);
public String getUsernameFromToken(String token);
public List getRolesFromToken(String token);
public Date getCreatedDateFromToken(String token);
public Date getExpirationDateFromToken(String token);
public Claims getClaimsFromToken(String token);
public Boolean isTokenExpired(String token);
public Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset);
public JwtAuthenticationResponse generateToken(String username);
public JwtAuthenticationResponse refreshToken(String token);
public Boolean validateToken(String token);
}
We also need to create the JwtUserDetailsServiceImpl Class that implements Spring Security’s interface UserDetailsService. This class overrides the loadUserByUsername method to check if the user exists or not.
@Service
@Transactional
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepo.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return JwtUserFactory.create(user);
}
}
}
Test It!
To test that everything works properly, we will create a REST Controller as bellow.
@RestController
@RequestMapping("/api/open/")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
@Qualifier(value = "jwtUtilWithoutDbCheckImpl")
private JwtUtil jwtTokenUtil;
@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
public ResponseEntity createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
// Perform the security
try{
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch(AuthenticationException e){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ServerResponse(e.getMessage()));
}
final JwtAuthenticationResponse token = jwtTokenUtil.generateToken(authenticationRequest.getUsername());
// Return the token
return ResponseEntity.ok(token);
}
}
What this method does, is it receives the user’s credentials then check them using an implementation of the AuthenticationManager Interface, if all is correct the user get a token from the server elsewhere he will receive an unauthorized message.
You will also need to define JwtAuthenticationRequest and JwtAuthenticationResponse DTOs classes.
public class JwtAuthenticationRequest implements Serializable {
private static final long serialVersionUID = -8445943548965154778L;
private String username;
private String password;
public JwtAuthenticationRequest() {
super();
}
public JwtAuthenticationRequest(String username, String password) {
this.setUsername(username);
this.setPassword(password);
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class JwtAuthenticationResponse implements Serializable {
private static final long serialVersionUID = 1250166508152483573L;
private final String token;
private final String refreshToken;
public JwtAuthenticationResponse(String token, String refreshToken) {
this.token = token;
this.refreshToken = refreshToken;
}
public String getToken() {
return this.token;
}
public String getRefreshToken() {
return refreshToken;
}
}
And here is another Controller to test things out.
@RestController
@RequestMapping("/api/auth/")
public class AuthenticatedUserController {
@Value("${jwt.header}")
private String tokenHeader;
@Autowired
@Qualifier(value = "jwtUtilWithoutDbCheckImpl")
private JwtUtil jwtTokenUtil;
@Autowired
private IUserService userService;
@RequestMapping(value = "me", method = RequestMethod.GET)
public ResponseEntity getAuthenticatedUser(@RequestHeader(value= "${jwt.header}") String token) {
Long userId = jwtTokenUtil.getUserIdFromToken(token);
User user = userService.read(userId);
return ResponseEntity.ok(user.getUsername());
}
}
Finally we need to implement the CommandLineRunner in our Application or Main class JwtAuthenticationApplication, so we can insert some users with their respective roles into database at runtime.
@SpringBootApplication
public class JwtAuthenticationApplication implements CommandLineRunner{
@Autowired
IUserService userService;
@Autowired
IRoleService roleService;
public static void main(String[] args) {
SpringApplication.run(JwtAuthenticationApplication.class, args);
}
@Override
@Transactional
public void run(String... args) throws Exception {
// TODO Auto-generated method stub
Role role_admin = new Role("ADMIN");
Role role_user = new Role("USER");
roleService.create(role_admin);
roleService.create(role_user);
User admin = new User("admin", "pass", true);
admin.addRole(role_admin);
admin.addRole(role_user);
User u = new User("user", "pass", true);
u.addRole(role_user);
userService.create(admin);
userService.create(u);
}
}
Conclusion:
In this article, we defined the two types or approachs of authentication, the session-based one and the stateless authentication and we have learned step by step the way to implement a stateless authentication system in our Spring Boot application using the JWT approach from scratch. Hope that this post was a great help for you. The full source code of this tutorial can be found on GitHub .
If you liked the content, share and please let me know in the comments section if you have any questions.