Login - Aligheri/jwt-owasp-based-starter GitHub Wiki
Login Authentication
This page documents the login authentication system implementation, including the core authentication service and controller endpoint.
Overview
The authentication system provides secure user login functionality with JWT token generation, fingerprinting for additional security, and comprehensive error handling. It follows Spring Security best practices and includes encryption for token protection.
Request and Response Interfaces
The authentication system uses generic interfaces to provide flexibility and allow for different implementations based on your specific needs.
AuthRequest Interface
public interface AuthRequest {
String getPassword();
String getIdentifier();
}
This interface defines the contract for authentication requests. Any login request object must implement these methods to provide user credentials.
DefaultAuthRequest Implementation
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DefaultAuthRequest implements AuthRequest {
@NotBlank
@Size(min = 3, max = 50)
private String username;
@NotBlank
@Size(min = 6)
private String password;
@Override
public String getIdentifier() {
return username;
}
}
The default implementation includes:
- Validation: Bean validation annotations for input validation
- Lombok Annotations: Automatic generation of getters, setters, and constructors
- Builder Pattern: Easy object construction
- Identifier Mapping: Maps username to the generic identifier field
AuthResponse Interface
public interface AuthResponse {
String getToken();
void setToken(String token);
}
This interface defines the contract for authentication responses, ensuring all response objects can handle JWT tokens.
DefaultAuthResponse Implementation
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DefaultAuthResponse implements AuthResponse {
private String token;
}
The default response implementation provides basic token storage with Lombok-generated methods.
Flexible Response Creation
The authentication service uses suppliers for flexible response object creation:
private final Supplier<R> authResponseSupplier;
private final Supplier<V> registerResponseSupplier;
This design pattern provides several benefits:
- Type Flexibility: Different response types can be used without changing the core logic
- Lazy Initialization: Response objects are created only when needed
- Extensibility: Easy to swap implementations for different use cases
- Dependency Injection: Suppliers can be injected to provide different response types
Example Supplier Configuration:
@Component
public class AuthServiceConfig {
@Bean
public Supplier<DefaultAuthResponse> authResponseSupplier() {
return DefaultAuthResponse::new;
}
@Bean
public Supplier<CustomAuthResponse> customAuthResponseSupplier() {
return CustomAuthResponse::new;
}
}
Core Authentication Method
Method Signature
R authenticateUser(T loginRequest, HttpServletResponse response, String issuerId);
Implementation
@Override
public R authenticateUser(T loginRequest, HttpServletResponse response, String issuerId) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getIdentifier(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String fingerprint = fingerprintUtils.generateFingerprint();
logger.debug("Generated fingerprint: {}", fingerprint);
cookieProvider.setFingerprintCookie(response, fingerprint);
String fingerprintHash = FingerprintUtils.hashFingerprint(fingerprint);
logger.debug("Generated fingerprint hash: {}", fingerprintHash);
String jwt = jwtTokenProvider.generateToken(loginRequest.getIdentifier(), fingerprintHash);
String cipheredJwt = tokenCipher.cipherToken(jwt);
logger.debug("Generated ciphered token: {}", cipheredJwt);
R authResponse = authResponseSupplier.get();
authResponse.setToken(cipheredJwt);
eventPublisher.publishEvent(
new AuthSuccessEvent(this, loginRequest.getIdentifier())
);
return authResponse;
} catch (AuthenticationException e) {
logger.warn("Auth failure for {}", loginRequest.getIdentifier());
throw new AuthenticationFailureException("Invalid credentials", e);
} catch (TokenEncyptionException e) {
logger.error("Token security breach", e);
throw new ServiceSecurityException("Token processing failure", e);
} catch (Exception e) {
logger.error("System authentication failure", e);
throw new AuthServiceException("Global auth failure", e);
}
}
Authentication Flow
The authentication process follows these steps:
- Credential Validation: Uses Spring Security's
AuthenticationManager
to validate username/password - Security Context: Sets the authenticated user in the security context
- Fingerprint Generation: Creates a unique browser fingerprint for additional security
- Cookie Setting: Stores the fingerprint in an HTTP-only cookie
- JWT Generation: Creates a JWT token with user identifier and fingerprint hash
- Token Encryption: Encrypts the JWT token for additional security
- Response Creation: Returns the encrypted token in the authentication response
- Event Publishing: Publishes a success event for audit/logging purposes
Controller Implementation
Endpoint Configuration
@PostMapping("/login")
public ResponseEntity<AuthResponse> loginUser(@Valid @RequestBody DefaultAuthRequest loginRequest, HttpServletResponse response) {
logger.info("Attempting to authenticate user: {}", loginRequest.getUsername());
return ResponseEntity.ok(authenticationService.authenticateUser(loginRequest, response, issuerId));
}
Usage Example
HTTP Request:
POST /login
Content-Type: application/json
{
"username": "[email protected]",
"password": "userPassword123"
}
Example Request Object:
DefaultAuthRequest loginRequest = DefaultAuthRequest.builder()
.username("[email protected]")
.password("userPassword123")
.build();
HTTP Response (Success):
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: fingerprint=abc123...; HttpOnly; Secure; SameSite=Strict
{
"token": "encrypted_jwt_token_here"
}
Example Response Object:
DefaultAuthResponse authResponse = DefaultAuthResponse.builder()
.token("encrypted_jwt_token_here")
.build();
Required Dependencies
To implement this authentication system, ensure you have the following components:
Service Dependencies
AuthenticationManager
- Spring Security authentication managerFingerprintUtils
- Utility for generating and hashing browser fingerprintsCookieProvider
- Service for managing HTTP cookiesJwtTokenProvider
- JWT token generation and validation serviceTokenCipher
- Token encryption/decryption serviceApplicationEventPublisher
- For publishing authentication events
Request/Response Objects
T loginRequest
- Generic login request object (must implementAuthRequest
interface)R authResponse
- Generic authentication response object (must implementAuthResponse
interface)DefaultAuthRequest
- Default implementation of login requestDefaultAuthResponse
- Default authentication responseSupplier<R> authResponseSupplier
- Flexible response object creation for different response types
Security Features
Multi-Layer Security
- Password Authentication: Standard username/password validation
- Browser Fingerprinting: Additional security layer to detect session hijacking
- Token Encryption: JWT tokens are encrypted before transmission
- HTTP-Only Cookies: Fingerprint stored in secure, HTTP-only cookies
- Comprehensive Logging: All authentication attempts are logged
Error Handling
The system handles three types of exceptions:
AuthenticationException
: Invalid credentialsTokenEncyptionException
: Token security issuesException
: General system failures
Configuration Requirements
Application Properties
# JWT Configuration
jwt.secret=your-secret-key
jwt.expiration=3600000
# Security Configuration
security.fingerprint.enabled=true
security.cookie.secure=true
security.cookie.httpOnly=true
Bean Configuration
@Configuration
public class AuthConfig {
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Testing
Unit Test Example
@Test
public void testSuccessfulAuthentication() {
// Given
DefaultAuthRequest request = DefaultAuthRequest.builder()
.username("[email protected]")
.password("password")
.build();
MockHttpServletResponse response = new MockHttpServletResponse();
// When
DefaultAuthResponse result = authenticationService.authenticateUser(request, response, "test-issuer");
// Then
assertThat(result.getToken()).isNotNull();
assertThat(response.getCookie("fingerprint")).isNotNull();
}
@Test
public void testAuthRequestValidation() {
// Test minimum username length
DefaultAuthRequest invalidRequest = DefaultAuthRequest.builder()
.username("ab") // Too short (min 3)
.password("password123")
.build();
Set<ConstraintViolation<DefaultAuthRequest>> violations = validator.validate(invalidRequest);
assertThat(violations).hasSize(1);
assertThat(violations.iterator().next().getMessage()).contains("size must be between 3 and 50");
}
Integration Test
@Test
@WithMockUser
public void testLoginEndpoint() throws Exception {
mockMvc.perform(post("/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"[email protected]\",\"password\":\"password\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists());
}
Troubleshooting
Common Issues
Authentication Failures
- Verify user credentials exist in the database
- Check password encoding matches the stored hash
- Ensure
AuthenticationManager
is properly configured
Token Issues
- Verify JWT secret is configured
- Check token cipher configuration
- Ensure fingerprint generation is working
Cookie Problems
- Verify cookie settings (secure, httpOnly, sameSite)
- Check domain and path configuration
- Ensure HTTPS is used in production