feat: add login with password verification and audit logging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package net.siegeln.cameleer.saas.auth;
|
|||||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||||
import net.siegeln.cameleer.saas.auth.dto.AuthResponse;
|
import net.siegeln.cameleer.saas.auth.dto.AuthResponse;
|
||||||
|
import net.siegeln.cameleer.saas.auth.dto.LoginRequest;
|
||||||
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
|
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -52,4 +53,30 @@ public class AuthService {
|
|||||||
|
|
||||||
return new AuthResponse(token, saved.getEmail(), saved.getName());
|
return new AuthResponse(token, saved.getEmail(), saved.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthResponse login(LoginRequest request, String sourceIp) {
|
||||||
|
var user = userRepository.findByEmail(request.email())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Invalid credentials"));
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(request.password(), user.getPassword())) {
|
||||||
|
auditService.log(
|
||||||
|
user.getId(), user.getEmail(), null,
|
||||||
|
AuditAction.AUTH_LOGIN_FAILED, null,
|
||||||
|
null, sourceIp,
|
||||||
|
"FAILURE", null
|
||||||
|
);
|
||||||
|
throw new IllegalArgumentException("Invalid credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = jwtService.generateToken(user);
|
||||||
|
|
||||||
|
auditService.log(
|
||||||
|
user.getId(), user.getEmail(), null,
|
||||||
|
AuditAction.AUTH_LOGIN, null,
|
||||||
|
null, sourceIp,
|
||||||
|
"SUCCESS", null
|
||||||
|
);
|
||||||
|
|
||||||
|
return new AuthResponse(token, user.getEmail(), user.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package net.siegeln.cameleer.saas.auth.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record LoginRequest(
|
||||||
|
@NotBlank @Email String email,
|
||||||
|
@NotBlank String password
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package net.siegeln.cameleer.saas.auth;
|
|||||||
|
|
||||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||||
|
import net.siegeln.cameleer.saas.auth.dto.LoginRequest;
|
||||||
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
|
import net.siegeln.cameleer.saas.auth.dto.RegisterRequest;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -12,6 +13,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
@@ -95,4 +97,75 @@ class AuthServiceTest {
|
|||||||
verify(userRepository, never()).save(any());
|
verify(userRepository, never()).save(any());
|
||||||
verify(auditService, never()).log(any(), any(), any(), any(), any(), any(), any(), any(), any());
|
verify(auditService, never()).log(any(), any(), any(), any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void login_returnsTokenForValidCredentials() {
|
||||||
|
var request = new LoginRequest("user@example.com", "password123");
|
||||||
|
var user = createUserWithId("user@example.com", "encoded-password");
|
||||||
|
|
||||||
|
when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user));
|
||||||
|
when(passwordEncoder.matches("password123", "encoded-password")).thenReturn(true);
|
||||||
|
when(jwtService.generateToken(user)).thenReturn("login-jwt-token");
|
||||||
|
|
||||||
|
var response = authService.login(request, "192.168.1.1");
|
||||||
|
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals("login-jwt-token", response.token());
|
||||||
|
assertEquals("user@example.com", response.email());
|
||||||
|
|
||||||
|
verify(auditService).log(
|
||||||
|
any(), eq("user@example.com"), eq(null),
|
||||||
|
eq(AuditAction.AUTH_LOGIN), eq(null),
|
||||||
|
eq(null), eq("192.168.1.1"),
|
||||||
|
eq("SUCCESS"), eq(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void login_rejectsInvalidPassword() {
|
||||||
|
var request = new LoginRequest("user@example.com", "wrong-password");
|
||||||
|
var user = createUserWithId("user@example.com", "encoded-password");
|
||||||
|
|
||||||
|
when(userRepository.findByEmail("user@example.com")).thenReturn(Optional.of(user));
|
||||||
|
when(passwordEncoder.matches("wrong-password", "encoded-password")).thenReturn(false);
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> authService.login(request, "192.168.1.1"));
|
||||||
|
|
||||||
|
// Verify AUTH_LOGIN_FAILED audit was logged
|
||||||
|
verify(auditService).log(
|
||||||
|
any(), eq("user@example.com"), eq(null),
|
||||||
|
eq(AuditAction.AUTH_LOGIN_FAILED), eq(null),
|
||||||
|
eq(null), eq("192.168.1.1"),
|
||||||
|
eq("FAILURE"), eq(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void login_rejectsUnknownEmail() {
|
||||||
|
var request = new LoginRequest("unknown@example.com", "password123");
|
||||||
|
|
||||||
|
when(userRepository.findByEmail("unknown@example.com")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
var exception = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> authService.login(request, "192.168.1.1"));
|
||||||
|
|
||||||
|
assertEquals("Invalid credentials", exception.getMessage());
|
||||||
|
verify(auditService, never()).log(any(), any(), any(), any(), any(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserEntity createUserWithId(String email, String password) {
|
||||||
|
var user = new UserEntity();
|
||||||
|
user.setEmail(email);
|
||||||
|
user.setName("Test User");
|
||||||
|
user.setPassword(password);
|
||||||
|
try {
|
||||||
|
var idField = UserEntity.class.getDeclaredField("id");
|
||||||
|
idField.setAccessible(true);
|
||||||
|
idField.set(user, UUID.randomUUID());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user