LDAP login issues fixed, added retry mechanism (#23690)

* LDAP login issues fixed, added retry mechanism

* Remove unwanted comments

* Addressed copilot comments

* Added integration TCs for ldap
This commit is contained in:
Ajith Prasad 2025-10-10 18:46:26 +05:30 committed by GitHub
parent e23638da60
commit 9d0a739c69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 585 additions and 17 deletions

View File

@ -1,5 +1,8 @@
package org.openmetadata.service.security.auth;
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
import static jakarta.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
import static org.openmetadata.service.security.SecurityUtil.writeJsonResponse;
import jakarta.servlet.http.HttpServletRequest;
@ -16,6 +19,7 @@ import org.openmetadata.schema.auth.TokenRefreshRequest;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.auth.JwtResponse;
import org.openmetadata.service.exception.CustomExceptionMessage;
import org.openmetadata.service.security.AuthServeletHandler;
@Slf4j
@ -109,9 +113,21 @@ public class LdapAuthServletHandler implements AuthServeletHandler {
resp.setContentType("application/json");
writeJsonResponse(resp, JsonUtils.pojoToJson(responseToClient));
} catch (CustomExceptionMessage e) {
LOG.error("LDAP login error: {}", e.getMessage(), e);
int statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
if (e.getResponse().getStatus() == SERVICE_UNAVAILABLE.getStatusCode()) {
statusCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
} else if (e.getResponse().getStatus() == UNAUTHORIZED.getStatusCode()) {
statusCode = HttpServletResponse.SC_UNAUTHORIZED;
} else if (e.getResponse().getStatus() == FORBIDDEN.getStatusCode()) {
statusCode = HttpServletResponse.SC_FORBIDDEN;
}
sendError(resp, statusCode, e.getMessage());
} catch (Exception e) {
LOG.error("Error handling LDAP login", e);
sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
LOG.error("Unexpected error handling LDAP login", e);
sendError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Authentication service error");
}
}
@ -225,7 +241,7 @@ public class LdapAuthServletHandler implements AuthServeletHandler {
private void sendError(HttpServletResponse resp, int status, String message) {
try {
resp.setStatus(status);
writeJsonResponse(resp, String.format("{\"error\":\"%s\"}", message));
writeJsonResponse(resp, String.format("{\"message\":\"%s\"}", message));
} catch (IOException e) {
LOG.error("Error writing error response", e);
}

View File

@ -3,6 +3,7 @@ package org.openmetadata.service.security.auth;
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
import static jakarta.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.schema.auth.TokenType.REFRESH_TOKEN;
@ -81,6 +82,8 @@ import org.springframework.util.CollectionUtils;
@Slf4j
public class LdapAuthenticator implements AuthenticatorHandler {
static final String LDAP_ERR_MSG = "[LDAP] Issue in creating a LookUp Connection ";
private static final int MAX_RETRIES = 3;
private static final int BASE_DELAY_MS = 500;
private RoleRepository roleRepository;
private UserRepository userRepository;
private TokenRepository tokenRepository;
@ -197,29 +200,74 @@ public class LdapAuthenticator implements AuthenticatorHandler {
@Override
public void validatePassword(String userDn, String reqPassword, User dummy)
throws TemplateException, IOException {
// performed in LDAP , the storedUser's name set as DN of the User in Ldap
// Retry configuration for connection establishment
final int maxRetries = 3;
final int baseDelayMs = 500;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
performLdapBind(userDn, reqPassword, dummy);
return; // Success
} catch (CustomExceptionMessage e) {
handleRetryableException(e, attempt, "LDAP connection failed for authentication");
}
}
throw new CustomExceptionMessage(
SERVICE_UNAVAILABLE,
"LDAP_CONNECTION_ERROR",
"Unable to connect to authentication server after " + maxRetries + " attempts.");
}
private void performLdapBind(String userDn, String reqPassword, User dummy)
throws TemplateException, IOException {
BindResult bindingResult = null;
LDAPConnection userConnection = null;
try {
bindingResult = ldapLookupConnectionPool.bind(userDn, reqPassword);
// Create a new connection for user authentication with proper SSL/TLS support
if (Boolean.TRUE.equals(ldapConfiguration.getSslEnabled())) {
// LDAPS (LDAP over SSL) - same configuration as connection pool
LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
LdapUtil ldapUtil = new LdapUtil();
SSLUtil sslUtil =
new SSLUtil(ldapUtil.getLdapSSLConnection(ldapConfiguration, connectionOptions));
userConnection =
new LDAPConnection(
sslUtil.createSSLSocketFactory(),
connectionOptions,
ldapConfiguration.getHost(),
ldapConfiguration.getPort());
} else {
userConnection =
new LDAPConnection(ldapConfiguration.getHost(), ldapConfiguration.getPort());
}
// Perform the bind operation
bindingResult = userConnection.bind(userDn, reqPassword);
if (Objects.equals(bindingResult.getResultCode().getName(), ResultCode.SUCCESS.getName())) {
return;
}
} catch (Exception ex) {
if (bindingResult != null
&& Objects.equals(
bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) {
if (Objects.equals(
bindingResult.getResultCode().getName(), ResultCode.INVALID_CREDENTIALS.getName())) {
recordFailedLoginAttempt(dummy.getEmail(), dummy.getName());
throw new CustomExceptionMessage(
UNAUTHORIZED, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
}
} catch (Exception ex) {
handleLdapException(ex);
} finally {
if (userConnection != null) {
userConnection.close();
}
}
if (bindingResult != null) {
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, bindingResult.getResultCode().getName());
} else {
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
}
LOG.error(
"LDAP bind failed with unexpected result: {}", bindingResult.getResultCode().getName());
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR,
"LDAP_AUTH_ERROR",
"Authentication failed: " + bindingResult.getResultCode().getName());
}
@Override
@ -237,6 +285,24 @@ public class LdapAuthenticator implements AuthenticatorHandler {
}
private String getUserDnFromLdap(String email) {
// Retry configuration (will be made configurable in future)
final int maxRetries = 3;
final int baseDelayMs = 500;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return performLdapUserSearch(email);
} catch (CustomExceptionMessage e) {
handleRetryableException(e, attempt, "LDAP connection failed for user lookup");
}
}
throw new CustomExceptionMessage(
SERVICE_UNAVAILABLE,
"LDAP_CONNECTION_ERROR",
"Unable to connect to authentication server after " + maxRetries + " attempts.");
}
private String performLdapUserSearch(String email) {
try {
Filter emailFilter =
Filter.createEqualityFilter(ldapConfiguration.getMailAttributeName(), email);
@ -270,7 +336,26 @@ public class LdapAuthenticator implements AuthenticatorHandler {
INTERNAL_SERVER_ERROR, INVALID_USER_OR_PASSWORD, INVALID_EMAIL_PASSWORD);
}
} catch (LDAPException ex) {
throw new CustomExceptionMessage(INTERNAL_SERVER_ERROR, "LDAP_ERROR", ex.getMessage());
ResultCode resultCode = ex.getResultCode();
String errorMessage = ex.getMessage();
// Check if it's a connection/network error
if (resultCode == ResultCode.CONNECT_ERROR
|| resultCode == ResultCode.SERVER_DOWN
|| resultCode == ResultCode.UNAVAILABLE
|| resultCode == ResultCode.TIMEOUT
|| errorMessage.contains("Connection refused")
|| errorMessage.contains("Connection reset")
|| errorMessage.contains("No route to host")) {
LOG.error("LDAP connection error for user lookup: {}", errorMessage);
throw new CustomExceptionMessage(
SERVICE_UNAVAILABLE,
"LDAP_CONNECTION_ERROR",
"Unable to connect to authentication server. Please try again later.");
}
LOG.error("LDAP error during user lookup: {}", errorMessage);
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, "LDAP_ERROR", "Authentication server error: " + errorMessage);
}
}
@ -486,4 +571,59 @@ public class LdapAuthenticator implements AuthenticatorHandler {
return newRefreshToken;
}
private void handleRetryableException(CustomExceptionMessage e, int attempt, String logMessage) {
if (e.getMessage().contains("Unable to connect to authentication server")
&& attempt < MAX_RETRIES) {
int delayMs = BASE_DELAY_MS * attempt;
LOG.warn(
"{} (attempt {}/{}). Retrying in {}ms...", logMessage, attempt, MAX_RETRIES, delayMs);
try {
Thread.sleep(delayMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
LOG.error("LDAP retry interrupted");
throw e;
}
} else {
throw e;
}
}
private void handleLdapException(Exception ex) throws TemplateException, IOException {
if (ex instanceof LDAPException ldapEx) {
ResultCode resultCode = ldapEx.getResultCode();
String errorMessage = ldapEx.getMessage();
// Check if it's a connection/network error
if (resultCode == ResultCode.CONNECT_ERROR
|| resultCode == ResultCode.SERVER_DOWN
|| resultCode == ResultCode.UNAVAILABLE
|| resultCode == ResultCode.TIMEOUT
|| errorMessage.contains("Connection refused")
|| errorMessage.contains("Connection reset")
|| errorMessage.contains("No route to host")) {
LOG.error("LDAP connection error during authentication: {}", errorMessage);
throw new CustomExceptionMessage(
SERVICE_UNAVAILABLE,
"LDAP_CONNECTION_ERROR",
"Unable to connect to authentication server. Please try again later.");
}
LOG.error("LDAP error during authentication: {}", errorMessage);
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR, "LDAP_ERROR", "Authentication server error: " + errorMessage);
} else if (ex instanceof GeneralSecurityException) {
LOG.error("SSL/TLS error during LDAP authentication", ex);
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR,
"LDAP_SSL_ERROR",
"SSL/TLS configuration error: " + ex.getMessage());
} else {
LOG.error("Unexpected error during LDAP authentication", ex);
throw new CustomExceptionMessage(
INTERNAL_SERVER_ERROR,
"LDAP_UNEXPECTED_ERROR",
"Unexpected authentication error: " + ex.getMessage());
}
}
}

View File

@ -0,0 +1,412 @@
package org.openmetadata.service.security.auth;
import static org.junit.jupiter.api.Assertions.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import java.time.Duration;
import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.jersey.client.ClientProperties;
import org.junit.jupiter.api.*;
import org.openmetadata.schema.auth.LoginRequest;
import org.openmetadata.service.OpenMetadataApplicationTest;
import org.openmetadata.service.util.TestUtils;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class LdapAuthCompleteFlowTest extends OpenMetadataApplicationTest {
private static final String AUTH_LOGIN_ENDPOINT = "/api/v1/auth/login";
private static final String AUTH_LOGOUT_ENDPOINT = "/api/v1/auth/logout";
private static GenericContainer<?> ldapContainer;
private static String ldapHost;
private static int ldapPort;
private static final String TEST_USER_EMAIL = "testuser@testcompany.com";
private static final String TEST_USER_PASSWORD = "testpass123";
@BeforeAll
@Override
public void createApplication() throws Exception {
startLdapContainer();
super.createApplication();
}
private void startLdapContainer() throws Exception {
LOG.info("Starting LDAP container...");
ldapContainer =
new GenericContainer<>(DockerImageName.parse("osixia/openldap:1.5.0"))
.withExposedPorts(389)
.withEnv("LDAP_ORGANISATION", "TestCompany")
.withEnv("LDAP_DOMAIN", "testcompany.com")
.withEnv("LDAP_ADMIN_PASSWORD", "adminpassword")
.withEnv("LDAP_CONFIG_PASSWORD", "config")
.withEnv("LDAP_READONLY_USER", "false")
.withEnv("LDAP_TLS", "false")
.withCommand("--copy-service", "--loglevel", "debug")
.withStartupTimeout(Duration.ofMinutes(2));
ldapContainer.start();
ldapHost = ldapContainer.getHost();
ldapPort = ldapContainer.getMappedPort(389);
LOG.info("LDAP container started at {}:{}", ldapHost, ldapPort);
Thread.sleep(10000);
loadTestLdapData();
}
private void loadTestLdapData() throws Exception {
LOG.info("Loading test LDAP data...");
LOG.info("Step 1: Creating organizational units");
String ouLdif =
"dn: ou=users,dc=testcompany,dc=com\n"
+ "objectClass: organizationalUnit\n"
+ "ou: users\n\n"
+ "dn: ou=groups,dc=testcompany,dc=com\n"
+ "objectClass: organizationalUnit\n"
+ "ou: groups";
execLdapAdd(ouLdif, "organizational units");
LOG.info("Step 2: Creating test user");
String userLdif =
"dn: uid=testuser,ou=users,dc=testcompany,dc=com\n"
+ "objectClass: inetOrgPerson\n"
+ "objectClass: posixAccount\n"
+ "objectClass: shadowAccount\n"
+ "uid: testuser\n"
+ "sn: User\n"
+ "givenName: Test\n"
+ "cn: Test User\n"
+ "displayName: Test User\n"
+ "uidNumber: 10001\n"
+ "gidNumber: 5000\n"
+ "userPassword: testpass123\n"
+ "loginShell: /bin/bash\n"
+ "homeDirectory: /home/testuser\n"
+ "mail: testuser@testcompany.com";
execLdapAdd(userLdif, "test user");
LOG.info("Step 3: Creating admin user");
String adminLdif =
"dn: uid=admin,ou=users,dc=testcompany,dc=com\n"
+ "objectClass: inetOrgPerson\n"
+ "objectClass: posixAccount\n"
+ "objectClass: shadowAccount\n"
+ "uid: admin\n"
+ "sn: Admin\n"
+ "givenName: System\n"
+ "cn: System Admin\n"
+ "displayName: System Admin\n"
+ "uidNumber: 10000\n"
+ "gidNumber: 5000\n"
+ "userPassword: admin123\n"
+ "loginShell: /bin/bash\n"
+ "homeDirectory: /home/admin\n"
+ "mail: admin@testcompany.com";
execLdapAdd(adminLdif, "admin user");
LOG.info("Step 4: Creating groups");
String groupsLdif =
"dn: cn=DataConsumer,ou=groups,dc=testcompany,dc=com\n"
+ "objectClass: groupOfNames\n"
+ "cn: DataConsumer\n"
+ "description: Data Consumer group\n"
+ "member: uid=testuser,ou=users,dc=testcompany,dc=com\n\n"
+ "dn: cn=admin,ou=groups,dc=testcompany,dc=com\n"
+ "objectClass: groupOfNames\n"
+ "cn: admin\n"
+ "description: Administrator group\n"
+ "member: uid=admin,ou=users,dc=testcompany,dc=com";
execLdapAdd(groupsLdif, "groups");
LOG.info("✓ All test LDAP data loaded successfully");
}
private void execLdapAdd(String ldifContent, String description) throws Exception {
String[] ldapAddCommand = {
"/bin/bash",
"-c",
"echo '"
+ ldifContent
+ "' | ldapadd -x -H ldap://localhost:389 -D 'cn=admin,dc=testcompany,dc=com' -w adminpassword"
};
org.testcontainers.containers.Container.ExecResult result =
ldapContainer.execInContainer(ldapAddCommand);
if (result.getExitCode() != 0) {
LOG.error("Failed to add {}: {}", description, result.getStderr());
throw new RuntimeException(
"Failed to load " + description + ". Exit code: " + result.getExitCode());
}
LOG.info("✓ Added {}", description);
}
@BeforeAll
void setupLdapConfiguration() throws Exception {
Thread.sleep(3000);
LOG.info("Application running on port: {}", APP.getLocalPort());
LOG.info("LDAP server running at: {}:{}", ldapHost, ldapPort);
updateAuthenticationConfigToLdap();
LOG.info("Test setup complete - LDAP authentication configured");
}
private void updateAuthenticationConfigToLdap() throws Exception {
LOG.info("Updating authentication configuration to LDAP...");
Map<String, Object> ldapConfig = new HashMap<>();
ldapConfig.put("host", ldapHost);
ldapConfig.put("port", ldapPort);
ldapConfig.put("dnAdminPrincipal", "cn=admin,dc=testcompany,dc=com");
ldapConfig.put("dnAdminPassword", "adminpassword");
ldapConfig.put("userBaseDN", "ou=users,dc=testcompany,dc=com");
ldapConfig.put("groupBaseDN", "ou=groups,dc=testcompany,dc=com");
ldapConfig.put("mailAttributeName", "mail");
ldapConfig.put("groupAttributeName", "objectClass");
ldapConfig.put("groupAttributeValue", "groupOfNames");
ldapConfig.put("groupMemberAttributeName", "member");
ldapConfig.put("allAttributeName", "*");
ldapConfig.put("roleAdminName", "admin");
ldapConfig.put("maxPoolSize", 10);
ldapConfig.put("sslEnabled", false);
Map<String, List<String>> roleMapping = new HashMap<>();
roleMapping.put("cn=admin,ou=groups,dc=testcompany,dc=com", Arrays.asList("admin"));
roleMapping.put(
"cn=DataConsumer,ou=groups,dc=testcompany,dc=com", Arrays.asList("DataConsumer"));
ldapConfig.put("authRolesMapping", new ObjectMapper().writeValueAsString(roleMapping));
ldapConfig.put("authReassignRoles", Arrays.asList("*"));
Map<String, Object> authConfig = new HashMap<>();
authConfig.put("provider", "ldap");
authConfig.put("providerName", "LDAP");
authConfig.put("publicKeyUrls", new ArrayList<>());
authConfig.put("clientId", "open-metadata");
authConfig.put("authority", "http://localhost:" + APP.getLocalPort());
authConfig.put("callbackUrl", "http://localhost:" + APP.getLocalPort() + "/callback");
authConfig.put("jwtPrincipalClaims", Arrays.asList("email", "preferred_username", "sub"));
authConfig.put("enableSelfSignup", true);
authConfig.put("ldapConfiguration", ldapConfig);
Map<String, Object> authorizerConfig = new HashMap<>();
authorizerConfig.put("className", "org.openmetadata.service.security.DefaultAuthorizer");
authorizerConfig.put("containerRequestFilter", "org.openmetadata.service.security.JwtFilter");
authorizerConfig.put("adminPrincipals", Arrays.asList("admin"));
authorizerConfig.put("testPrincipals", new ArrayList<>());
authorizerConfig.put("allowedEmailRegistrationDomains", Arrays.asList("all"));
authorizerConfig.put("principalDomain", "open-metadata.org");
authorizerConfig.put("allowedDomains", new ArrayList<>());
authorizerConfig.put("enforcePrincipalDomain", false);
authorizerConfig.put("enableSecureSocketConnection", false);
authorizerConfig.put("useRolesFromProvider", false);
Map<String, Object> securityConfig = new HashMap<>();
securityConfig.put("authenticationConfiguration", authConfig);
securityConfig.put("authorizerConfiguration", authorizerConfig);
Invocation.Builder request =
APP.client()
.target(getServerUrl() + "/api/v1/system/security/config")
.request(MediaType.APPLICATION_JSON);
for (Map.Entry<String, String> entry : TestUtils.ADMIN_AUTH_HEADERS.entrySet()) {
request = request.header(entry.getKey(), entry.getValue());
}
Response response = request.put(Entity.json(securityConfig));
LOG.info("Auth config update response status: {}", response.getStatus());
if (response.getStatus() == 200 || response.getStatus() == 201) {
LOG.info("✓ Successfully updated authentication configuration to LDAP");
Thread.sleep(3000);
} else {
String error = response.readEntity(String.class);
LOG.error("Failed to update authentication configuration: {}", error);
fail("Failed to update authentication configuration - Status: " + response.getStatus());
}
}
@Test
@Order(1)
void testLdapContainerStartup() {
LOG.info("Testing LDAP container startup");
assertNotNull(ldapContainer);
assertTrue(ldapContainer.isRunning());
assertNotNull(ldapHost);
assertTrue(ldapPort > 0);
LOG.info("LDAP container is running at {}:{}", ldapHost, ldapPort);
}
@Test
@Order(3)
void testSuccessfulLoginWithValidCredentials() throws Exception {
LOG.info("Testing successful LDAP login with valid credentials");
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(TEST_USER_EMAIL);
loginRequest.setPassword(Base64.getEncoder().encodeToString(TEST_USER_PASSWORD.getBytes()));
Response response =
APP.client()
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
.request(MediaType.APPLICATION_JSON)
.property(ClientProperties.FOLLOW_REDIRECTS, false)
.post(Entity.json(loginRequest));
LOG.info("Login response status: {}", response.getStatus());
assertEquals(200, response.getStatus(), "Login should succeed with valid credentials");
String responseBody = response.readEntity(String.class);
LOG.info("Login response: {}", responseBody);
Map<String, Object> jwtResponse = new ObjectMapper().readValue(responseBody, Map.class);
assertNotNull(jwtResponse.get("accessToken"), "Access token should be present");
assertEquals("Bearer", jwtResponse.get("tokenType"), "Token type should be Bearer");
assertNotNull(jwtResponse.get("expiryDuration"), "Expiry duration should be present");
assertNull(jwtResponse.get("refreshToken"), "Refresh token should not be sent to client");
Map<String, NewCookie> cookies = response.getCookies();
LOG.info("Session cookies: {}", cookies.keySet());
}
@Test
@Order(4)
void testFailedLoginWithInvalidPassword() throws Exception {
LOG.info("Testing failed LDAP login with invalid password");
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(TEST_USER_EMAIL);
loginRequest.setPassword(Base64.getEncoder().encodeToString("wrongpassword".getBytes()));
Response response =
APP.client()
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
.request(MediaType.APPLICATION_JSON)
.property(ClientProperties.FOLLOW_REDIRECTS, false)
.post(Entity.json(loginRequest));
LOG.info("Login response status: {}", response.getStatus());
assertNotEquals(200, response.getStatus(), "Login should fail with invalid password");
assertTrue(
response.getStatus() == 401 || response.getStatus() == 500,
"Should return 401 or 500 for invalid credentials");
}
@Test
@Order(5)
void testFailedLoginWithNonExistentUser() throws Exception {
LOG.info("Testing failed LDAP login with non-existent user");
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail("nonexistent@testcompany.com");
loginRequest.setPassword(Base64.getEncoder().encodeToString("password".getBytes()));
Response response =
APP.client()
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
.request(MediaType.APPLICATION_JSON)
.property(ClientProperties.FOLLOW_REDIRECTS, false)
.post(Entity.json(loginRequest));
LOG.info("Login response status: {}", response.getStatus());
assertNotEquals(200, response.getStatus(), "Login should fail with non-existent user");
}
@Test
@Order(7)
void testLogout() throws Exception {
LOG.info("Testing logout flow");
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(TEST_USER_EMAIL);
loginRequest.setPassword(Base64.getEncoder().encodeToString(TEST_USER_PASSWORD.getBytes()));
Response loginResponse =
APP.client()
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
.request(MediaType.APPLICATION_JSON)
.property(ClientProperties.FOLLOW_REDIRECTS, false)
.post(Entity.json(loginRequest));
assertEquals(200, loginResponse.getStatus(), "Login should succeed");
Map<String, NewCookie> cookies = loginResponse.getCookies();
Invocation.Builder logoutRequest =
APP.client()
.target(getServerUrl() + AUTH_LOGOUT_ENDPOINT)
.request(MediaType.APPLICATION_JSON)
.property(ClientProperties.FOLLOW_REDIRECTS, false);
for (NewCookie cookie : cookies.values()) {
logoutRequest = logoutRequest.cookie(cookie);
}
Response logoutResponse = logoutRequest.get();
LOG.info("Logout response status: {}", logoutResponse.getStatus());
assertEquals(200, logoutResponse.getStatus(), "Logout should succeed");
}
@Test
@Order(9)
void testMultipleLoginAttempts() throws Exception {
LOG.info("Testing multiple failed login attempts");
String testEmail = "multitest@testcompany.com";
for (int i = 0; i < 3; i++) {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(testEmail);
loginRequest.setPassword(Base64.getEncoder().encodeToString("wrongpassword".getBytes()));
Response response =
APP.client()
.target(getServerUrl() + AUTH_LOGIN_ENDPOINT)
.request(MediaType.APPLICATION_JSON)
.property(ClientProperties.FOLLOW_REDIRECTS, false)
.post(Entity.json(loginRequest));
LOG.info("Attempt {} - Response status: {}", i + 1, response.getStatus());
assertNotEquals(200, response.getStatus(), "Login should fail with wrong password");
Thread.sleep(500);
}
LoginAttemptCache.getInstance().recordSuccessfulLogin(testEmail);
}
private String getServerUrl() {
return "http://localhost:" + APP.getLocalPort();
}
@AfterAll
void cleanup() {
if (ldapContainer != null && ldapContainer.isRunning()) {
LOG.info("Stopping LDAP container");
ldapContainer.stop();
}
}
}