Securing REST APIs is a big deal in modern web development, especially when dealing with sensitive data and microservices. One of the best ways to do this is by using JSON Web Tokens (JWTs) with Spring Security. Here’s a down-to-earth guide on how to lock down your REST APIs using JWT and Spring Security.
The Basics: JWT and Spring Security
Let’s start with JWTs. JSON Web Tokens are a standard way to safely send information between two parties as a JSON object. A JWT has three parts: a header, payload, and signature. The header tells you which algorithm is used for signing the token. The payload contains the data that’s being sent, and the signature is created by encrypting the header and payload with a secret key.
Now, let’s talk Spring Security. This is a powerful framework for securing Spring-based applications. It meshes well with other Spring tech, so it’s a go-to for many developers. Spring Security supports all kinds of authentication and is all about declarative security programming, meaning you can set security rules without writing too much code.
Kicking Off a Spring Boot Project
To get started securing your REST APIs, you need a Spring Boot app. You can kick off a new project using Spring Initializr or your go-to IDE. Just make sure you include essentials like Spring Web
and Spring Security
in your pom.xml
if you’re using Maven.
Bringing in JWT Dependencies
You’ll need some specific dependencies for handling JWTs. These include jjwt-api
, jjwt-impl
, and jjwt-jackson
from the io.jsonwebtoken
group. Add these bad boys to your project like this:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
Setting Up Spring Security
Spring Security has session-based authentication by default, which is fine for traditional web apps but not for REST APIs. You need to tweak it for stateless authentication using JWT. This means setting up a custom security configuration class.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtUtil));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Making the JWT Utility
Next up, you’ll need a utility class for creating and validating JWT tokens. This class will handle generating the token using user details and the secret key, plus checking incoming tokens.
@Component
public class JwtUtil {
private final String SECRET_KEY = "your-secret-key";
public String generateToken(UserDetails userDetails) {
Claims claims = Jwts.claims().setSubject(userDetails.getUsername());
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 1 day
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public boolean validateToken(String token, UserDetails userDetails) {
return userDetails.getUsername().equals(extractUsername(token)) && !isTokenExpired(token);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
Handling Authentication and Authorization
When it comes to authentication, the client sends in a request with a username and password. The server checks these credentials and, if they’re legit, generates a JWT token. This token is then sent back to the client, which includes it in the Authorization
header of future requests.
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()));
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(jwt));
}
}
Locking Down REST Endpoints
To secure your endpoints, you can use the @RolesAllowed
annotation or create custom security checks in your controllers. The JwtAuthenticationFilter
class will verify the JWT token in every incoming request, allowing access only if the token checks out.
@RestController
public class SimpleController {
@RolesAllowed("ADMIN")
@GetMapping("/admin-only")
public String adminOnly() {
return "Hello, Admin!";
}
@RolesAllowed("USER")
@GetMapping("/user-only")
public String userOnly() {
return "Hello, User!";
}
}
Walking Through the JWT Flow
Here’s the step-by-step flow of JWT:
- Client Authentication: The client sends a request with the username and password.
- Token Generation: The server validates the credentials and, if they’re valid, creates a JWT token.
- Token Response: The server sends the token back to the client.
- Client Request: The client adds the JWT token to the
Authorization
header of future requests. - Server Validation: The server checks the JWT token for each incoming request, granting access only if it’s valid.
Pros and Cons
Using JWTs with Spring Security has its perks. It’s perfect for stateless security policies, which are great for REST APIs. However, it does have its downsides. For instance, you can’t manage server-side logout since JWTs aren’t stored on the server.
Securing REST APIs with JWT and Spring Security is a solid and efficient approach. By following these steps and understanding the basics, your APIs will be safe from unauthorized access. Always safeguard your secret keys and consider using OAuth 2.0 Resource Server for advanced setups.