Skip to main content
The Chat Server API uses JWT (JSON Web Token) authentication with BCrypt password hashing to secure user accounts and API endpoints.

Token generation

When a user registers or logs in, the server generates a JWT token containing the user’s ID and username. The token is signed using HMAC-SHA with a secret key and includes an expiration timestamp.
public String register(RegisterUserRequest userRequest) {
    // ... validation logic ...
    
    User newUser = new User(
        username,
        encoder.encode(userRequest.getPassword())
    );
    
    userRepository.save(newUser);
    
    return JwtUtil.generateToken(newUser.getId(), newUser.getUsername());
}

public String login(LoginUserRequest loginRequest) {
    User user = userRepository.findByUsername(loginRequest.getUsername())
        .orElseThrow(UsernameOrPasswordIncorrectException::new);
    
    if (!BCrypt.checkpw(loginRequest.getPassword(), user.getPassword())) 
        throw new UsernameOrPasswordIncorrectException();
    
    return JwtUtil.generateToken(user.getId(), user.getUsername());
}
Tokens are valid for 30 days by default (JWT_EXPIRATION_MS = 1000 * 60 * 60 * 30)

Password hashing with BCrypt

User passwords are hashed using BCrypt before being stored in the database. BCrypt automatically handles salt generation and is resistant to rainbow table and brute-force attacks.
User newUser = new User(
    username,
    encoder.encode(userRequest.getPassword())
);
The encoder is Spring Security’s PasswordEncoder configured to use BCrypt.

Authorization header

All protected endpoints require a valid JWT token in the Authorization header.

Request format

GET /users/@me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The token should be prefixed with Bearer (with a space). The JWT filter automatically strips this prefix when validating tokens.

Token validation

The JwtAuthenticationFilter intercepts all requests and validates the JWT token:
JwtAuthenticationFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request, 
                                HttpServletResponse response, 
                                FilterChain filterChain) throws ServletException, IOException {
    final String authHeader = request.getHeader("Authorization");
    
    try {
        if (authHeader != null && !authHeader.isBlank()) {
            String userId = JwtUtil.getUserIdFromToken(authHeader);
            
            SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(
                    userId, 
                    null, 
                    List.of(new SimpleGrantedAuthority(("ROLE_USER")))
                )
            );
        }
    } catch (Exception e) {
        logger.warn("Continuing without authentication: " + e.getMessage());
    }
    
    filterChain.doFilter(request, response);
}

Extracting user information

The getUserIdFromToken method parses and validates the JWT token:
JwtUtil.java
public static String getUserIdFromToken(String token) {
    if (token.startsWith("Bearer ")) token = token.substring(7);
    Claims claims = validateToken(token);
    return claims.get("userId", String.class);
}

public static Claims validateToken(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(Configuration.JWT_SECRET.getBytes())
        .build()
        .parseClaimsJws(token)
        .getBody();
}

Configuration

Authentication settings are defined in Configuration.java:
Configuration.java
public class Configuration {
    // Password must be at least 6 characters
    public static final int MIN_PASSWORD_LENGTH = 6;
    
    // Username must be 2-8 characters
    public static final int MIN_USERNAME_LENGTH = 2;
    public static final int MAX_USERNAME_LENGTH = 8;
    
    // JWT secret (should be set via environment variable)
    public static final String JWT_SECRET = 
        System.getenv("JWT_SECRET") == null 
            ? "dGhpcy1wYXNzc3dvcmQtaXMtc3VwZXItZHVwZXItc2VjcmV0..." 
            : System.getenv("JWT_SECRET");
    
    // Token expires after 30 days
    public static final long JWT_EXPIRATION_MS = 1000 * 60 * 60 * 30;
}
The JWT_SECRET should always be set via the JWT_SECRET environment variable in production. The default value is only for development.

Protected vs public endpoints

The security configuration defines which endpoints require authentication:
DefaultSecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(session -> 
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/users/register").permitAll()
            .requestMatchers("/users/login").permitAll()
            .requestMatchers("/v3/*").permitAll()
            .anyRequest().authenticated()
        );
    
    http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
}

Public endpoints

  • POST /users/register - Create new account
  • POST /users/login - Authenticate and receive token
  • /v3/* - API documentation

Protected endpoints

All other endpoints require a valid JWT token in the Authorization header.

Error handling

Returned when:
  • No Authorization header is provided
  • Token is expired or invalid
  • Token signature verification fails
{
  "message": "Unauthorized"
}
Returned when:
  • Username or password is incorrect during login
  • Username is too short (< 2 characters) or too long (> 8 characters)
  • Password is too short (< 6 characters)
{
  "message": "Username or password incorrect"
}

Build docs developers (and LLMs) love