DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Authentication With Remote LDAP Server in Spring WebFlux
  • Spring Security Oauth2: Google Login
  • How To Implement OAuth2 Security in Microservices
  • Authentication with Spring Boot and Spring Security — JWT and Postgres

Trending

  • Unlocking AI Coding Assistants Part 4: Generate Spring Boot Application
  • Unlocking the Benefits of a Private API in AWS API Gateway
  • Unlocking AI Coding Assistants Part 3: Generating Diagrams, Open API Specs, And Test Data
  • Testing SingleStore's MCP Server
  1. DZone
  2. Coding
  3. Frameworks
  4. Authentication With Remote LDAP Server in Spring Web MVC

Authentication With Remote LDAP Server in Spring Web MVC

A guide for Java developers who want to integrate Spring Boot MVC applications with a remote LDAP server and authenticate/authorize their users with JWT.

By 
Taner Inal user avatar
Taner Inal
·
Updated Jan. 15, 22 · Tutorial
Likes (8)
Comment
Save
Tweet
Share
17.7K Views

Join the DZone community and get the full member experience.

Join For Free

There are plenty of articles, videos, and courses about this topic, but nearly all of them use embedded LDAP as a source for user information. In this article, we will develop a Spring Boot project and integrate to remote LDAP through Spring Security. In addition, we will perform authentication (auth) and authorization (autz) operations over JWT (JSON Web Token) for the APIs we will open. 

In a business scenario, our application serves as a user portal service that authenticates and authorizes users against specific APIs with their LDAP authorities. First, let's talk about the terms we will use.

  • Authentication (Auth): Validation of your credentials (i.e., Username/ID and password) to verify your identity. In short, it is about WHO you are.
  • Authorization (Autz): Authorization takes place after auth and is for determining the access ability of authenticated users to resources and to what extent. In short, it is about WHAT you can do.
  • JWT (JSON Web Token): An open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. You may get detailed information from its official website.
  • LDAP: Lightweight Directory Access Protocol is an industry-standard application protocol for accessing and maintaining distributed directory information services. A common use of LDAP is to provide a central place to store user information such as username, password, organizational info (team, manager, etc.).

Development Environment Information

For the sake of being on the same page, below is our development environment information. Version information for project dependencies is discussed in the next section.

  • OS: Windows 10 20H2
  • IDE: IntelliJ IDEA 2019.3.1 (Ultimate Edition)
  • Java: 1.8
  • Spring Boot: 2.5.6
  • Apache Maven: 3.5.0

Preparing the Spring Boot Project

We are initializing our Spring Boot project from Spring Initializr.

Spring Initializr setup.

The dependencies shown above are not the ultimate list. We will add needed dependencies while walking through the following titles.

LDAP Integration

We will integrate to a remote LDAP server to authenticate the user and retrieve information (roles, manager, email, etc.) about him to authorize for specific resources. In order to see what is in the directory, we use Apache Directory Studio (Version: 2.0.0.v20210213-M16). 

Apache Directory Studio sample screen.

In order to integrate with the LDAP server, we should add the following dependency to our project:

XML
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
</dependency>


With the above steps, we have prepared our application for talking to a remote LDAP server. We'll cover how to talk with it for auth/autz purposes in later sections.

Before we deliver code samples, it is useful to know that field declarations, imports, and Javadocs have been cropped out from the code samples below to keep this article as fluent as possible. Please refer to the GitHub repo for full versions.

Preparing APIs

For this project, we have two roles: FINANCE and BUSINESS. We authorize these roles for paths /finance-zone and /business-zone respectively. We prepare FinanceController and BusinessController for this purpose as follows:

Java
 
@RestController
@RequestMapping("/finance-zone")
public class FinanceController {
    @GetMapping
    public ResponseEntity<String> financeMethod() {
            return ResponseEntity.ok("Congrats! If you see this, you have FINANCE role...");
    }
}

@RestController
@RequestMapping("/business-zone")
public class BusinessController {
    @GetMapping
    public ResponseEntity<String> businessMethod() {
            return ResponseEntity.ok("Congrats! If you see this, you have BUSINESS role...");
    }
}


Now it is time to describe how authentication is being handled. 

Authentication is done by querying the remote LDAP server, with the client's LDAP username/password being sent to the /authentication API. This is a @RestController, which calls PortalUserService (that is actually a service class implementing UserDetailsService of Spring Security Core. This service is also used in the authorization phase.) 

Java
 
@RestController
@RequestMapping("/authenticate")
@RequiredArgsConstructor
@Slf4j
public class AuthenticateController {
    private final PortalUserService portalUserService;
  
    @PostMapping
    public ResponseEntity<AuthResponse> authenticate(@RequestBody @NonNull AuthRequest authRequest) {
            log.info("Authentication request for user {} received!", authRequest.getUsername());
            return ResponseEntity.ok(portalUserService.authenticateUser(authRequest.getUsername(), authRequest.getPassword()));
    }
}


Introducing Auth/Autz Exception Handling Mechanisms

For unauthenticated requests (i.e., access attempts with expired JWT tokens) we prepare UnauthenticatedEntryPoint to handle this case and return the descriptive response to the client. Here is the code:

Java
 
@Configuration
@Slf4j
public class UnauthenticatedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
        log.info("Access Denied. Unauthenticated access attempt to resource {} from {} Authorization header: {}",
                httpServletRequest.getRequestURI(),
                Optional.ofNullable(httpServletRequest.getHeader(Constants.HEADER_AUTHORIZATION))
                        .orElse(httpServletRequest.getRemoteAddr()),
                httpServletRequest.getHeader(Constants.HEADER_AUTHORIZATION));
        httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST,
                "It seems whether you're trying to access a resource without authenticating. Please authenticate first then try again!");
    }
}


For this scenario, the client will receive a "HttpStatus.400 (Bad Request)" response since the authentication info being sent is bad.

For unauthorized requests (i.e., access attempts to an API that is not allowed by that user's role), we prepare AccessDeniedEntryPoint to handle this case and return the descriptive response to the client. Here is the code:

Java
 
@Configuration
@Slf4j
public class AccessDeniedEntryPoint implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {
        log.info("Access Denied. Unauthorized access attempt to resource {} Authorization header: {}",
                httpServletRequest.getRequestURI(),
                httpServletRequest.getHeader(Constants.HEADER_AUTHORIZATION));
        httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(),
                "Access Denied. It seems you're trying to access a resource that you are not allowed! ");
    }
}


We'll use both mechanisms while configuring security in later topics.

Introducing JWT Handling Mechanism

The /authentication API, described under previous sections, returns a JWT to the client. The client should send this token in the Authorization request header with the "Bearer" prefix while consuming APIs. In this phase, our application validates and extracts JWT from the request and executes authorization checks by the data enclosed in that token. (All data should not be stored in tokens for security purposes. We'll extract the username from the token and query the LDAP server for the roles of that user. Authorization checks are being executed on that data.)

Our JwtUtil class, copied (partially) below, sits in the center of the logic described above:

Java
 
@Service
@Slf4j
public class PortalUserService implements UserDetailsService {
    private static final String LDAP_ATTRIBUTE_USERPASSWORD = "userpassword";
    private BaseLdapPathContextSource contextSource;

    @PostConstruct
    private void prepareLdapContext() {
        String ldapFullUrl = new StringBuilder(this.ldapUrl)
                .append(":")
                .append(this.ldapPort)
                .append("/")
                .append(this.ldapRoot)
                .toString();

        DefaultSpringSecurityContextSource localContextSource = new DefaultSpringSecurityContextSource(ldapFullUrl);
        localContextSource.setUserDn(this.ldapManagerDn);
        localContextSource.setPassword(this.ldapManagerPassword);
        localContextSource.afterPropertiesSet();
        this.contextSource = localContextSource;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        try {
            log.info("Searching LDAP for user {}", username);
            SearchControls searchControls = new SearchControls();
            searchControls.setReturningAttributes(new String[]{Constants.LDAP_ATTRIBUTE_ISMEMBEROF, Constants.LDAP_ATTRIBUTE_UID});
            SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(this.contextSource);
            template.setSearchControls(searchControls);

            DirContextOperations searchResult = template.searchForSingleEntry(this.ldapUserSearchBase, this.ldapUserSearchFilter, new String[]{username});
            List<String> grantedAuthorities = new ArrayList<>(this.getGrantedAuthorities(searchResult));
            log.info("User {} retrieved. User's roles are: {}", username, grantedAuthorities);

            return new PortalUserPrincipal(PortalUser.builder()
                    .username(username)
                    .grantedAuthorities(grantedAuthorities)
                    .build());
        } catch (IncorrectResultSizeDataAccessException ex) {
            log.error("Unexpected result size returned from LDAP for search for user {}", username);

            if (ex.getActualSize() == 0) {
                throw new UsernameNotFoundException("User " + username + " not found in LDAP.");
            } else {
                throw ex;
            }
        }
    }

    public AuthResponse authenticateUser(String username, String password) {
        Assert.isTrue(StringUtils.isNotBlank(username), "Username should not left blank!");
        Assert.isTrue(StringUtils.isNotBlank(password), "Password should not left blank!");

        List<String> grantedAuthorities = this.doLdapSearch(username, password);
        log.info("Authentication of {} successfull! Users groups are: {}", username, grantedAuthorities);
        PortalUserPrincipal portalUserPrincipal = new PortalUserPrincipal(PortalUser.builder()
                .username(username)
                .grantedAuthorities(grantedAuthorities)
                .build());
        Authentication authentication = new UsernamePasswordAuthenticationToken(portalUserPrincipal, null, portalUserPrincipal.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        List<String> userRoles = portalUserPrincipal.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        return AuthResponse.builder()
                .username(username)
                .message(Constants.MESSAGE_SUCCESS)
                .status(Constants.STATUS_CODE_SUCCESS)
                .userRoles(userRoles)
                .userPermissions(new ArrayList<>())
                .token(JwtUtils.createJWTToken(username, this.jwtSecret, this.jwtTimeout, userRoles))
                .build();
    }

    private List<String> doLdapSearch (String username, String password) {
        try {
            PortalUserPrincipal portalUserPrincipal = new PortalUserPrincipal(PortalUser.builder().username(username).build());
            Authentication authentication = new UsernamePasswordAuthenticationToken(portalUserPrincipal, password);
            PasswordComparisonAuthenticator passwordComparisonAuthenticator = new PasswordComparisonAuthenticator(this.contextSource);
            passwordComparisonAuthenticator.setPasswordEncoder(new LdapShaPasswordEncoder());
            passwordComparisonAuthenticator.setUserDnPatterns(new String[]{this.ldapUserSearchFilter + "," + ldapUserSearchBase});
            passwordComparisonAuthenticator.setUserAttributes(new String[]{Constants.LDAP_ATTRIBUTE_ISMEMBEROF, LDAP_ATTRIBUTE_USERPASSWORD});

            DirContextOperations authenticationResult = passwordComparisonAuthenticator.authenticate(authentication);

            return this.getGrantedAuthorities(authenticationResult);
        } catch (BadCredentialsException e) {
            log.error("LDAP authentication failed for {}. Wrong password!", username);
            throw e;
        } catch (UsernameNotFoundException e) {
            log.error("LDAP authentication failed for {}. No such user!", username);
            throw e;
        }
    }

    private List<String> getGrantedAuthorities(DirContextOperations ldapResult) {
        if (ArrayUtils.isEmpty(ldapResult.getStringAttributes(Constants.LDAP_ATTRIBUTE_ISMEMBEROF))) {
            log.info("No roles found for user: {}. Returning empty granted authorities list.", ldapResult.getStringAttribute(Constants.LDAP_ATTRIBUTE_UID));

            return  new ArrayList<>();
        }

        return Arrays.asList(ldapResult.getStringAttributes(Constants.LDAP_ATTRIBUTE_ISMEMBEROF)).stream()
                .filter(groupDn -> StringUtils.endsWith(groupDn, this.groupBase))
                .map(groupDn -> StringUtils.substringBetween(StringUtils.upperCase(groupDn), "CN=", ","))
                .collect(Collectors.toList());
    }
}


Configuring Spring Security

This is the point where we should describe the core of this whole mechanism: WebSecurityConfiguration. In this class, we configure the security of our web application under the configure method:

Java
 
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    private final JwtRequestFilter jwtRequestFilter;
    private final UnauthenticatedEntryPoint unauthenticatedEntryPoint;
    private final AccessDeniedEntryPoint accessDeniedEntryPoint;

    @Value("${autz.permitted.paths.finance}")
    private String[] financeRolePermittedPaths;
    @Value("${autz.permitted.paths.business:}")
    private String[] businessRolePermittedPaths;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                .and()
                .csrf()
                    .disable()
                .authorizeRequests()
                    .antMatchers(financeRolePermittedPaths).hasAuthority(Constants.LDAP_ROLE_FINANCE)
                    .antMatchers(businessRolePermittedPaths).hasAuthority(Constants.LDAP_ROLE_BUSINESS)
                    .anyRequest().authenticated()
                .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(this.unauthenticatedEntryPoint)
                    .accessDeniedHandler(this.accessDeniedEntryPoint)
                .and()
                    .logout()
                        .disable()
                    .formLogin()
                        .disable();

        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}


The following list explains the above code:

  1. Cors and csrf strategies are declared. We disabled csrf since we use JWT for authorization in each and every request.
  2. Under authorizeRequests(), we declared "who has access to where." Authorization checks are done according to these rules.
  3. Since we authorize each request with JWT, we don't need a session. We declared the session creation policy as Stateless. (This also works well if you don't share sessions between application servers.)
  4. What will happen when an authentication or authorization error occurs? This question is being answered under exceptionHandling(). We covered the contents of these classes in previous sections. 
  5. At last, we disabled the built-in login mechanism of Spring Security, since we provided the  /authentication API. We also disabled the logout mechanism, because it is beyond the scope of this article. 

Testing

We test our application with Postman. You can find the exported Postman collection in my GitHub repository.

First things first; we authenticate our Finance user and retrieve the token:
Screenshot showing user authentication.

Here is the response we received:Screenshot of user authentication response.

Now its time to call the /finance API with the above token:Screenshot of the Finance API.

Here is the response we received;
Response returned for Finance API.

So what if we call the /business API with our Finance user? Here is the request:
Screenshot showing /business API request.

Here is the response:
Response to /business API request.

Conclusion

In this article, we developed a Spring Boot project and integrated to a remote LDAP through Spring Security. In addition, we performed authentication and authorization operations over JWT for the APIs we opened. 

You can get the full implementation of this article from my GitHub repository.

For the Spring WebFlux version of this concept please refer to my next article.

Spring Framework authentication workplace Web Service Spring Security Spring Boot application JWT (JSON Web Token) intellij

Opinions expressed by DZone contributors are their own.

Related

  • Authentication With Remote LDAP Server in Spring WebFlux
  • Spring Security Oauth2: Google Login
  • How To Implement OAuth2 Security in Microservices
  • Authentication with Spring Boot and Spring Security — JWT and Postgres

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!