Skip to main content

Overview

The User Management System uses JWT (JSON Web Tokens) for stateless authentication. Security is configured through Spring Security with a custom JWT filter chain.

JWT Configuration

JWT settings are configured in the profile-specific properties files.

Development Settings

application-dev.properties
jwt.secret=dev_secret
jwt.expiration=86400000

Production Settings

application-prod.properties
jwt.secret=prod_secret
jwt.expiration=86400000
The default secrets shown above are examples only. Always use strong, randomly generated secrets in production!

Secret Key Setup

Generating a Secure Secret

Your JWT secret should be at least 256 bits (32 characters) long. Generate a secure secret:
openssl rand -base64 32

Setting the Secret

export JWT_SECRET=your-generated-secret-key-here
export JWT_EXPIRATION=86400000
Using environment variables is the recommended approach for production deployments to avoid committing secrets to version control.

Token Expiration Settings

The jwt.expiration property defines token lifetime in milliseconds.

Common Expiration Times

DurationMillisecondsConfiguration
15 minutes900000jwt.expiration=900000
1 hour3600000jwt.expiration=3600000
24 hours86400000jwt.expiration=86400000
7 days604800000jwt.expiration=604800000
30 days2592000000jwt.expiration=2592000000

Recommendations

  • Short-lived tokens (15-60 min): High security applications
  • Medium-lived tokens (1-24 hours): Standard applications
  • Long-lived tokens (7-30 days): Mobile applications with refresh token strategy

JWT Utility Class

The JwtUtil class handles token generation and validation:
JwtUtil.java
@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String SECRET;

    @Value("${jwt.expiration}")
    private long EXPIRATION_DATE;

    public String generateToken(String username, String role) {
        return JWT.create()
                .withSubject(username)
                .withClaim("role", role)
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_DATE))
                .sign(Algorithm.HMAC256(SECRET));
    }

    public DecodedJWT validateToken(String token) {
        try {
            return JWT.require(Algorithm.HMAC256(SECRET))
                    .build()
                    .verify(token);
        } catch (JWTVerificationException exception) {
            return null;
        }
    }
}
Key Features:
  • Uses HMAC256 algorithm for signing
  • Stores username in the subject claim
  • Stores user role in a custom role claim
  • Validates token signature and expiration

Security Filter Chain

The security configuration is defined in SecurityConfig.java:
SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/auth/**").permitAll()
                        .requestMatchers("/users/me").hasAnyAuthority("ROLE_USER")
                        .requestMatchers("/admin/users").hasAnyAuthority("ROLE_ADMIN")
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() { 
        return new BCryptPasswordEncoder(); 
    }
}

Security Features

CSRF is disabled because JWT authentication is stateless and doesn’t use cookies.
.csrf(AbstractHttpConfigurer::disable)
Sessions are disabled for true stateless authentication.
.sessionManagement(session -> 
    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
BCrypt is used for secure password hashing with a default strength of 10.
@Bean
PasswordEncoder passwordEncoder() { 
    return new BCryptPasswordEncoder(); 
}

Authorization Rules

Access control is configured per endpoint:
Endpoint PatternRequired AuthorityDescription
/auth/**None (permitAll)Authentication endpoints
/users/meROLE_USERUser profile access
/admin/usersROLE_ADMINAdmin user management

Adding Custom Rules

Add new authorization rules in the SecurityFilterChain:
.authorizeHttpRequests(authz -> authz
    .requestMatchers("/auth/**").permitAll()
    .requestMatchers("/users/me").hasAnyAuthority("ROLE_USER")
    .requestMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN")
    .requestMatchers("/api/public/**").permitAll()
    .anyRequest().authenticated()
)

JWT Authentication Filter

The JwtAuthenticationFilter extracts and validates JWT tokens from requests:
JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) 
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        String token = null;

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
        }

        if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            String username = jwtUtil.getUsernameFromToken(token);
            String role = jwtUtil.getRoleFromToken(token);

            UsernamePasswordAuthenticationToken authToken = 
                new UsernamePasswordAuthenticationToken(
                    username,
                    null,
                    Collections.singletonList(new SimpleGrantedAuthority(role))
                );

            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
        filterChain.doFilter(request, response);
    }
}

Filter Flow

  1. Extract token from Authorization header
  2. Validate token using JwtUtil
  3. Extract claims (username and role)
  4. Create authentication object
  5. Set security context for the request
  6. Continue filter chain

Using JWT Tokens

Authentication Request

curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin-user",
    "password": "admin123"
  }'

Response

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Authenticated Request

curl -X GET http://localhost:8080/users/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Security Best Practices

Strong Secrets

Use randomly generated secrets of at least 256 bits. Never use default or predictable secrets in production.

Short Expiration

Set appropriate token expiration times. Implement refresh tokens for longer sessions.

HTTPS Only

Always use HTTPS in production to prevent token interception.

Secret Rotation

Implement a strategy for rotating JWT secrets periodically.

Environment-Specific Security

# Relaxed settings for development
jwt.secret=dev_secret_change_in_production
jwt.expiration=86400000
spring.console.enabled=true

Troubleshooting

Token Validation Fails

Check:
  • Secret matches between token generation and validation
  • Token hasn’t expired
  • Token format is correct (Bearer prefix)
  • Token hasn’t been tampered with

Unauthorized Access

Verify:
  • Token is included in Authorization header
  • Token has required role/authority
  • Endpoint requires authentication
  • Filter chain is properly configured

Invalid Signature

Causes:
  • JWT secret changed after token generation
  • Token modified after signing
  • Wrong algorithm used for verification

Build docs developers (and LLMs) love