Skip to main content

Overview

The Authorization Service uses JWT (JSON Web Tokens) for stateless authentication. Users authenticate once with credentials and receive a token that grants access to protected endpoints.

Authentication Flow

Login Process

The login process is handled by the AuthService (auth/application/services/AuthService.java:18).

Step 1: Credential Validation

@Service
public class AuthService {
    
    public AuthResponse login(LoginRequest request) {
        // 1. Normalize email
        String email = request.email().trim().toLowerCase();
        
        // 2. Find user by email
        var userOpt = userRepositoryPort.findByEmail(new UserEmail(email));
        if (userOpt.isEmpty()) {
            throw new BadCredentialsException("Credenciales erróneas");
        }
        
        var user = userOpt.get();
        
        // 3. Verify password
        boolean matches = passwordEncoder.matches(
            request.password(), 
            user.getPassword().value()
        );
        
        if (!matches) {
            throw new BadCredentialsException("Credenciales erróneas");
        }
        
        // 4. Check account status
        if (!user.getStatus().isEnabled()) {
            throw new DisabledException("Cuenta deshabilitada");
        }
        
        // 5. Generate JWT
        String token = jwtUtil.generateToken(user);
        return new AuthResponse(token);
    }
}

Password Encoding

Passwords are hashed using BCrypt with a strength factor configured in SecurityConfig:
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
BCrypt automatically:
  • Generates a salt for each password
  • Applies multiple rounds of hashing
  • Produces a hash like: $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

Password Migration Support

The service includes logic to migrate plain-text passwords (auth/application/services/AuthService.java:44-52):
if (!matches) {
    // Check if password is plain text (for migration)
    boolean storedLooksLikeBcrypt = stored.startsWith("$2a$") || 
                                    stored.startsWith("$2b$") || 
                                    stored.startsWith("$2y$");
    
    if (!storedLooksLikeBcrypt && stored.equals(request.password())) {
        // Re-hash and persist
        String rehashed = passwordEncoder.encode(stored);
        user.changePassword(new UserPassword(rehashed));
        userRepositoryPort.save(user);
        matches = true;
    }
}

JWT Token Generation

Tokens are created by JwtUtil (security/util/JwtUtil.java:18).

Token Structure

public String generateToken(UserDomain user) {
    Date now = new Date();
    Date exp = new Date(now.getTime() + expirationMs);
    
    // Extract roles
    List<String> roles = user.getRoles().stream()
        .map(r -> r.getName().value())
        .collect(Collectors.toList());
    
    // Extract permissions (flatten from roles)
    List<String> permissions = user.getRoles().stream()
        .flatMap(r -> r.getPermissions().stream())
        .map(p -> p.getName().value())
        .distinct()
        .collect(Collectors.toList());
    
    return Jwts.builder()
        .setSubject(user.getEmail().value())  // email as subject
        .claim("roles", roles)
        .claim("permissions", permissions)
        .claim("userId", user.getUserId().id())
        .claim("email", user.getEmail().value())
        .setIssuedAt(now)
        .setExpiration(exp)
        .signWith(key)  // HMAC-SHA256 signing
        .compact();
}

JWT Claims

The token contains the following claims:
ClaimDescriptionExample
subSubject (user email)"admin@example.com"
emailUser email (duplicate for convenience)"admin@example.com"
userIdUser UUID"550e8400-e29b-41d4-a716-446655440000"
rolesList of role names["ADMIN", "USER"]
permissionsList of permission names (from all roles)["READ_USERS", "WRITE_USERS"]
iatIssued at timestamp1709553600
expExpiration timestamp1709557200

Signing Key

The JWT is signed using HMAC-SHA256 with a secret key:
public JwtUtil(@Value("${jwt.secret}") String secret,
               @Value("${jwt.expiration-ms:3600000}") long expirationMs) {
    this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    this.expirationMs = expirationMs;
}
Configuration in application.yml:
jwt:
  secret: "your-256-bit-secret-key-here-must-be-long-enough"
  expiration-ms: 3600000  # 1 hour
The JWT secret must be at least 256 bits (32 characters) for HS256 algorithm. Use a cryptographically secure random string in production.

Request Authentication

Incoming requests are authenticated by JwtAuthenticationFilter (security/filter/JwtAuthenticationFilter.java:29).

Filter Chain

The filter is registered in the Spring Security filter chain (security/config/SecurityConfig.java:48):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, RequestIdFilter requestIdFilter) {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/api/auth/**", "/swagger-ui/**").permitAll()
            .anyRequest().authenticated()
        )
        .sessionManagement(session -> 
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
}

Token Extraction and Validation

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) {
        // 1. Extract token from Authorization header
        final String authHeader = request.getHeader("Authorization");
        String jwt = null;
        
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            jwt = authHeader.substring(7).trim();
        }
        
        // 2. Validate token
        if (jwt != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            String username = jwtUtil.extractUsername(jwt);
            
            if (username != null && jwtUtil.validateToken(jwt)) {
                // 3. Extract claims
                Claims claims = jwtUtil.extractAllClaims(jwt);
                List<String> roles = claims.get("roles", List.class);
                List<String> permissions = claims.get("permissions", List.class);
                
                // 4. Create authorities
                Collection<GrantedAuthority> authorities = new ArrayList<>();
                roles.forEach(role -> 
                    authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
                );
                permissions.forEach(perm -> 
                    authorities.add(new SimpleGrantedAuthority("PERM_" + perm))
                );
                
                // 5. Create authentication object
                String userId = claims.get("userId", String.class);
                AuthPrincipal principal = new AuthPrincipal(username, userId);
                
                UsernamePasswordAuthenticationToken authToken = 
                    new UsernamePasswordAuthenticationToken(principal, null, authorities);
                
                // 6. Set security context
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        
        filterChain.doFilter(request, response);
    }
}

Token Validation

The validateToken method checks expiration:
public boolean validateToken(String token) {
    try {
        Claims claims = extractAllClaims(token);
        return !claims.getExpiration().before(new Date());
    } catch (Exception e) {
        return false;  // Invalid signature or malformed token
    }
}

Authority Mapping

Roles and permissions are prefixed when added to Spring Security authorities:
  • Roles: ROLE_ADMIN, ROLE_USER
  • Permissions: PERM_READ_USERS, PERM_WRITE_USERS
This allows using both @PreAuthorize("hasRole('ADMIN')") and @PreAuthorize("hasAuthority('PERM_READ_USERS')") annotations.

AuthPrincipal

The custom AuthPrincipal class (security/service/AuthPrincipal.java) stores user identity:
public class AuthPrincipal {
    private final String username;  // email
    private final String userId;    // UUID
    
    public String getName() {
        return username;
    }
    
    public String getUserId() {
        return userId;
    }
}
Access the current user in controllers:
@GetMapping("/me")
public UserResponse getCurrentUser(@AuthenticationPrincipal AuthPrincipal principal) {
    UUID userId = UUID.fromString(principal.getUserId());
    return userService.findById(userId);
}

Security Configuration

Public Endpoints

These endpoints don’t require authentication (security/config/SecurityConfig.java:38-44):
  • POST /api/auth/** - Login endpoints
  • POST /api/users - User registration
  • /swagger-ui/** - API documentation
  • /v3/api-docs/** - OpenAPI specification

Protected Endpoints

All other endpoints require a valid JWT token:
# Without token - 401 Unauthorized
curl http://localhost:8080/api/users

# With token - 200 OK
curl http://localhost:8080/api/users \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Stateless Sessions

The service is completely stateless:
.sessionManagement(session -> 
    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
This means:
  • No server-side session storage
  • Tokens must be sent with every request
  • Horizontal scaling is simplified
  • No session cookies are used

Error Handling

Authentication errors return appropriate HTTP status codes:
ErrorStatus CodeReason
Invalid credentials401Wrong email or password
Account disabled401User account is inactive
Missing token401No Authorization header
Invalid token401Malformed or expired JWT
Expired token401Token past expiration time

Best Practices

Token Expiration

Set appropriate expiration times (1 hour recommended) and implement token refresh if needed

Secure Secret

Use a strong, randomly generated secret key and store it securely (environment variables, secrets manager)

HTTPS Only

Always transmit tokens over HTTPS to prevent interception

Token Storage

Store tokens securely on the client (avoid localStorage, prefer httpOnly cookies or memory)

Next Steps

RBAC System

Learn how roles and permissions control access

Login API

See the login endpoint documentation

Security Config

Configure JWT settings for your deployment

Build docs developers (and LLMs) love