Demystifying JWT Tokens: The Key to Secure Web Applications
As a software engineer, you are responsible for ensuring the security of the web applications you develop. One way to achieve this is through the use of JWT (JSON Web Tokens) tokens, an open standard for transmitting information securely between parties as a JSON object. In this article, we will explore what JWT tokens are, how they work, and how to use them in Java for authentication and authorization.
What are JWT Tokens and How Do They Work?
A JWT token consists of three parts: a header, a payload, and a signature. The header contains metadata about the token, such as the type of token and the signing algorithm used. The payload contains claims, which are statements about an entity (typically a user) and additional data. The signature is used to verify the message wasn’t changed along the way and, in the case of tokens signed with a secret key, to verify that the sender of the JWT is who it says it is and to ensure that the message wasn’t changed along the way.
The process of using JWT tokens for authentication and authorization begins with a user logging into the web application. Once the user’s credentials have been verified, the server generates a JWT token that contains information about the user, such as their username and role(s). The token is then sent back to the client, where it is typically stored in local storage or a cookie.
On subsequent requests to the server, the client sends the JWT token in the request headers. The server then verifies the token’s signature and reads the claims in the payload to determine whether the user is authorized to access the requested resource. If the token is valid, the server responds with the requested resource. If the token is invalid, the server returns a 401 Unauthorized response.
How to Use JWT Tokens in Java
Implementing JWT token authentication and authorization in a Java web application involves several steps:
- Include the JWT library in your project. You can do this using a build tool such as Maven or Gradle, or by manually adding the library to your classpath.
- Create a JWT token using the Jwts builder class. The builder allows you to set the token’s claims and sign it with a secret key.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
String secretKey = "mysecretkey";
String token = Jwts.builder()
.setSubject("user123")
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes())
.compact();
- Validate a JWT token using the Jwts parser class. The parser allows you to verify the token’s signature and read its claims.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
String secretKey = "mysecretkey";
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.QUVXRGFzdGluZmFjaWxpdHlWYWx1ZQ";
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey.getBytes())
.parseClaimsJws(token)
.getBody();
String subject = claims.getSubject();
System.out.println("User: " + subject);
} catch (SignatureException e) {
System.out.println("Invalid token");
}
4.Configure JWT in your Spring Security configuration. Spring Security provides built-in support for JWT token
authentication and authorization. You can configure it in your Spring Security configuration by defining a
JwtAuthenticationFilter
and a JwtAuthorizationFilter
.
The JwtAuthenticationFilter
is responsible for authenticating the user by verifying the JWT token’s signature and
creating an authentication object, which Spring Security uses for further authorization checks.
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String secretKey = "mysecretkey";
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher("/login"));
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getHeader("Authorization").replace("Bearer ", "");
Claims claims = Jwts.parser()
.setSigningKey(secretKey.getBytes())
.parseClaimsJws(token)
.getBody();
String subject = claims.getSubject();
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(subject, null));
}
}
The JwtAuthorizationFilter
is responsible for authorizing the user by reading the JWT token’s claims and checking
whether the user is authorized to access the requested resource.
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private String secretKey = "mysecretkey";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = parseJwt(request);
if (jwt != null && Jwts.parser().setSigningKey(secretKey.getBytes()).isSigned(jwt)) {
Claims claims = Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(jwt).getBody();
String subject = claims.getSubject();
List<String> roles = claims.get("roles", List.class);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(subject, null, roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}
return null;
}
}
In your Spring Security configuration, you can then define these filters and specify the authentication and authorization providers.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter())
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
.and().csrf().disable();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.builder().username("admin").password(passwordEncoder().encode("password")).roles("ADMIN").build(),
User.builder().username("user").password(passwordEncoder().encode("password")).roles("USER").build()
);
}
}
In this example, we use a BCryptPasswordEncoder
to encode the passwords of the users stored in memory.
You can replace this with your own implementation of a password encoder and a user details service that retrieves
users from a database or another source.
Conclusion
JWT tokens provide a secure way to authenticate and authorize users in web applications. They allow for stateless authentication and authorization, which simplifies the implementation of web applications and can improve performance. In this article, we’ve covered the basics of JWT tokens and shown how to implement JWT token authentication and authorization in a Java web application using Spring Security.