How can you implement a conditional login mechanism between LDAP and local Database using Spring Boot.
Tags: webdev, springboot, ldap, postgressql
Published: 2024-07-05 07:25:57

Implementing a conditional login mechanism in Spring Boot that seamlessly switches between LDAP authentication and local database authentication based on the availability of the LDAP server is a crucial aspect of ensuring uninterrupted access to your application. 

By incorporating this dynamic authentication approach, you can enhance the reliability and resilience of your system, providing users with a seamless login experience even during LDAP server downtime.

To implement a conditional login mechanism that checks whether the LDAP server is available and, based on that, either authenticate using the LDAP server or fall back to a local PostgreSQL database, you will need to:

1. **Check LDAP server availability**: Implement a mechanism to check if the LDAP server is reachable.
2. **Attempt LDAP Authentication**: If the LDAP server is available, authenticate using LDAP.
3. **Fallback to Local Authentication**: If the LDAP server is not reachable, authenticate using credentials stored in the local PostgreSQL database.


_Here are the Step-by-Step Implementation_ :

## 1. Add Dependencies :
Here I am considering the Maven as build tool.Ensure you have the following dependencies in your
pom.xml
file:

`<dependencies><!-- Spring Boot and Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot and Data JPA --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- PostgreSQL Driver --><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency><!-- Spring Boot LDAP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-ldap</artifactId></dependency><!-- Spring Boot Actuator (Optional) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies>`

## 

## 2. Configure Application Properties

Set the necessary properties in application.properties:

```
# LDAP Server Configuration
ldap.url=ldap://url:389
ldap.admin.dn=cn=admin,dc=urdc,dc=urdc
ldap.admin.password=ldappassword
ldap.search.base=dc=cspurdc,dc=csp

# PostgreSQL Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/yourdb
spring.datasource.username=yourusername
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update

# User Authentication Configuration
user.samaccountname=username
user.password=ldappassword
```

## 

## 3. Create User Entity and Repository for PostgreSQL

Define a simple User entity and repository for the PostgreSQL database. **User.java**

```
package com.example.ldapauth.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String username;
    private String password;
    // Getters and Setters
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

```
**UserRepository.java**

```
package com.example.ldapauth.repository;

import com.example.ldapauth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}
```

### 4. Service for Local Authentication

Add a service to handle local database authentication.
**LocalAuthService.java**

```
package com.example.ldapauth.service;

import com.example.ldapauth.entity.User;
import com.example.ldapauth.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LocalAuthService {

    @Autowired
    private UserRepository userRepository;

    public boolean authenticate(String username, String password) {
        User user = userRepository.findByUsername(username);
        return user != null && user.getPassword().equals(password);
    }
}
```

## 5. Modify LdapService to Include Health Check:

Add a method to check the availability of the LDAP server.
**LdapService.java**

```
package com.example.ldapauth.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.stereotype.Service;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;

@Service
public class LdapService {

    @Autowired
    private DirContext ldapContext;

    @Value("${ldap.search.base}")
    private String searchBase;

    @Value("${user.samaccountname}")
    private String sAMAccountName;

    @Value("${user.password}")
    private String userPassword;

    public boolean isLdapAvailable() {
        try {
            // Try a no-op search to check server availability
            NamingEnumeration<SearchResult> results = ldapContext.search("", "(objectClass=*)", new SearchControls());
            return results != null;  // If this does not throw an exception, LDAP is available
        } catch (NamingException e) {
            return false; // LDAP is not available
        }
    }

    public String getUserDn(String username) {
        try {
            String searchFilter = "(sAMAccountName=" + username + ")";
            SearchControls searchControls = new SearchControls();
            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            NamingEnumeration<SearchResult> results = ldapContext.search(searchBase, searchFilter, searchControls);

            if (results.hasMore()) {
                SearchResult searchResult = results.next();
                return searchResult.getNameInNamespace();
            }
        } catch (NamingException e) {
            e.printStackTrace();
        }
        return null;
    }

    public boolean authenticateUser(String userDn, String password) {
        if (userDn == null) {
            System.out.println("User authentication failed: User DN not found.");
            return false;
        }

        try {
            Hashtable<String, String> env = new Hashtable<>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            env.put(Context.PROVIDER_URL, ldapContext.getEnvironment().get(Context.PROVIDER_URL).toString());
            env.put(Context.SECURITY_AUTHENTICATION, "simple");
            env.put(Context.SECURITY_PRINCIPAL, userDn);
            env.put(Context.SECURITY_CREDENTIALS, password);

            DirContext ctx = new InitialDirContext(env);
            ctx.close();
            System.out.println("User authentication successful.");
            return true;
        } catch (NamingException e) {
            System.out.println("User authentication failed: " + e.getMessage());
            return false;
        }
    }
}
```

## 6. Modify Controller to Implement Conditional Login
**LdapController.java**

```
package com.example.ldapauth.controller;

import com.example.ldapauth.service.LdapService;
import com.example.ldapauth.service.LocalAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LdapController {

    @Autowired
    private LdapService ldapService;

    @Autowired
    private LocalAuthService localAuthService;

    @PostMapping("/login")
    public String performLogin(@RequestParam String username, @RequestParam String password) {
        if (ldapService.isLdapAvailable()) {
            String userDn = ldapService.getUserDn(username);
            boolean isAuthenticated = ldapService.authenticateUser(userDn, password);
            if (isAuthenticated) {
                return "LDAP authentication successful!";
            }
        }
        
        boolean isLocalAuthenticated = localAuthService.authenticate(username, password);
        if (isLocalAuthenticated) {
            return "Local authentication successful!";
        }

        return "Authentication failed!";
    }
}
```

## Explanation:

1. Check LDAP Availability:
   isLdapAvailable method in LdapService performs a dummy LDAP 
   search to check if the LDAP server is available.
2. Conditional Login Logic:
   In LdapController, the /login endpoint checks LDAP 
   availability.
   If LDAP is available, it tries to authenticate the user with 
   LDAP.
   If LDAP authentication fails or LDAP is not available, it 
   falls back to local PostgreSQL database authentication using 
   LocalAuthService.
3. Local Authentication:
   If LDAP is unavailable or the user fails LDAP authentication, 
   it checks the local PostgreSQL database for user credentials 
   using UserRepository.
4. REST Controller:
   The @PostMapping on /login endpoint handles login requests and 
   applies the conditional logic for authentication.

This setup provides a robust mechanism to switch between LDAP and local authentication based on the availability of the LDAP server.

I hope this was helpful guys. Please note that you have to modify few things based on your requirements and logic. I have given here overall idea only. 

Happy Coding :) 

Thanks,
JavaCharter
Kailash Nirmal
Written on : 5th July 2024.