SecuritySpring SecurityOAuth2JWTSpring Boot

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.

5 min read
Share:

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:

  1. Authorization Code Flow: Used for server-side applications
  2. Implicit Flow: For client-side applications (deprecated)
  3. Client Credentials Flow: For service-to-service communication
  4. 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

  1. Use HTTPS: Always use HTTPS in production
  2. Secure JWT Secret: Store secrets in environment variables
  3. Token Expiration: Set reasonable expiration times
  4. Refresh Tokens: Implement refresh token mechanism
  5. Input Validation: Validate all inputs
  6. Rate Limiting: Implement rate limiting on auth endpoints
  7. 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.