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:

  1. Credential Validation: Uses Spring Security's AuthenticationManager to validate username/password
  2. Security Context: Sets the authenticated user in the security context
  3. Fingerprint Generation: Creates a unique browser fingerprint for additional security
  4. Cookie Setting: Stores the fingerprint in an HTTP-only cookie
  5. JWT Generation: Creates a JWT token with user identifier and fingerprint hash
  6. Token Encryption: Encrypts the JWT token for additional security
  7. Response Creation: Returns the encrypted token in the authentication response
  8. 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 manager
  • FingerprintUtils - Utility for generating and hashing browser fingerprints
  • CookieProvider - Service for managing HTTP cookies
  • JwtTokenProvider - JWT token generation and validation service
  • TokenCipher - Token encryption/decryption service
  • ApplicationEventPublisher - For publishing authentication events

Request/Response Objects

  • T loginRequest - Generic login request object (must implement AuthRequest interface)
  • R authResponse - Generic authentication response object (must implement AuthResponse interface)
  • DefaultAuthRequest - Default implementation of login request
  • DefaultAuthResponse - Default authentication response
  • Supplier<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 credentials
  • TokenEncyptionException: Token security issues
  • Exception: 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

Related Documentation