Securing Spring Boot Applications with OAuth2 and JWT
A comprehensive guide to implementing secure authentication and authorization in Spring Boot applications using OAuth2 and JWT.
Securing Spring Boot Applications with OAuth2 and JWT
Security is a critical aspect of any application. In this post, I'll demonstrate how to implement robust authentication and authorization in Spring Boot using OAuth2 and JWT tokens.
Understanding OAuth2 and JWT
OAuth2 Overview
OAuth2 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service. It works by:
- Authorization Code Flow: Used for server-side applications
- Implicit Flow: For client-side applications (deprecated)
- Client Credentials Flow: For service-to-service communication
- Resource Owner Password Flow: For trusted applications
JWT (JSON Web Tokens)
JWTs are compact, URL-safe tokens that represent claims to be transferred between parties. They consist of three parts:
- Header: Algorithm and token type
- Payload: Claims (user data, expiration, etc.)
- Signature: Cryptographic signature for verification
Project Setup
Dependencies
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2 Resource Server -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JWT Support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
JWT Utility Class
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty");
}
return false;
}
}
JWT Authentication Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Security Configuration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exception ->
exception
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler())
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig
) throws Exception {
return authConfig.getAuthenticationManager();
}
}
Authentication Controller
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserRepository userRepository;
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> authenticateUser(
@Valid @RequestBody LoginRequest loginRequest
) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
User user = userRepository.findById(userPrincipal.getId())
.orElseThrow(() -> new ResourceNotFoundException("User", "id", userPrincipal.getId()));
return ResponseEntity.ok(new JwtAuthenticationResponse(
jwt,
user.getId(),
user.getName(),
user.getEmail(),
user.getRoles()
));
}
@PostMapping("/register")
public ResponseEntity<ApiResponse> registerUser(
@Valid @RequestBody SignUpRequest signUpRequest
) {
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return new ResponseEntity<>(
new ApiResponse(false, "Email address already in use!"),
HttpStatus.BAD_REQUEST
);
}
User user = new User(
signUpRequest.getName(),
signUpRequest.getEmail(),
signUpRequest.getPassword()
);
user.setRoles(Collections.singletonList(Role.ROLE_USER));
User result = userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/api/v1/users/{id}")
.buildAndExpand(result.getId()).toUri();
return ResponseEntity.created(location)
.body(new ApiResponse(true, "User registered successfully"));
}
}
Role-Based Access Control
@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users")
@PreAuthorize("hasAuthority('user:read')")
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userRepository.findAll();
return ResponseEntity.ok(users);
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasAuthority('user:delete')")
public ResponseEntity<ApiResponse> deleteUser(@PathVariable Long id) {
userRepository.deleteById(id);
return ResponseEntity.ok(new ApiResponse(true, "User deleted successfully"));
}
}
OAuth2 Integration
OAuth2 Configuration
@Configuration
@EnableOAuth2Client
public class OAuth2Config {
@Bean
public OAuth2RestTemplate oauth2RestTemplate(
OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details
) {
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
@Bean
public OAuth2ProtectedResourceDetails google() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId("your-client-id");
details.setClientSecret("your-client-secret");
details.setUserAuthorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
details.setAccessTokenUri("https://oauth2.googleapis.com/token");
details.setScope(Arrays.asList("email", "profile"));
return details;
}
}
OAuth2 Authentication Controller
@RestController
@RequestMapping("/api/v1/oauth2")
public class OAuth2Controller {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserRepository userRepository;
@GetMapping("/google/callback")
public ResponseEntity<JwtAuthenticationResponse> googleCallback(
@RequestParam("code") String code
) {
// Exchange code for access token
OAuth2AccessToken accessToken = getGoogleAccessToken(code);
// Get user info from Google
GoogleUserInfo userInfo = getGoogleUserInfo(accessToken);
// Find or create user
User user = userRepository.findByEmail(userInfo.getEmail())
.orElseGet(() -> createOAuthUser(userInfo));
// Generate JWT token
Authentication authentication = createAuthentication(user);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(
jwt,
user.getId(),
user.getName(),
user.getEmail(),
user.getRoles()
));
}
private User createOAuthUser(GoogleUserInfo userInfo) {
User user = new User();
user.setName(userInfo.getName());
user.setEmail(userInfo.getEmail());
user.setProvider(AuthProvider.GOOGLE);
user.setProviderId(userInfo.getId());
user.setRoles(Collections.singletonList(Role.ROLE_USER));
return userRepository.save(user);
}
}
Security Best Practices
- Use HTTPS: Always use HTTPS in production
- Secure JWT Secret: Store secrets in environment variables
- Token Expiration: Set reasonable expiration times
- Refresh Tokens: Implement refresh token mechanism
- Input Validation: Validate all inputs
- Rate Limiting: Implement rate limiting on auth endpoints
- Audit Logging: Log all authentication attempts
Conclusion
Implementing OAuth2 and JWT in Spring Boot provides a robust security foundation for your applications. This setup supports both traditional username/password authentication and OAuth2 social login.
In future posts, I'll cover advanced topics like multi-factor authentication, OAuth2 resource server configuration, and integrating with popular identity providers like Auth0 and Okta.
Related Modules
Building Scalable REST APIs with Spring Boot
Learn how to design and implement scalable REST APIs using Spring Boot with best practices for performance, security, and maintainability.
Implementing Event-Driven Architecture with Kafka
A practical guide to building event-driven microservices using Apache Kafka and Spring Boot for scalable, decoupled systems.