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:
Claim Description Example 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 timestamp 1709553600expExpiration timestamp 1709557200
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:
Error Status Code Reason Invalid credentials 401 Wrong email or password Account disabled 401 User account is inactive Missing token 401 No Authorization header Invalid token 401 Malformed or expired JWT Expired token 401 Token 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