avatarIvan Franchin

Summarize

Implementing and Securing a Spring Boot GraphQL API with LDAP

A complete guide on implementing a Spring Boot GraphQL API and securing it with LDAP

Photo by Kenny Eliason on Unsplash

In this article, we will guide you step-by-step through the implementation process of a Spring Boot application named Book API. This application exposes a secure GraphQL API, which will be protected using LDAP.

In case you are unfamiliar:

  • GraphQL is a query language and runtime for APIs. It provides a concise way for clients to request and receive data from servers. With GraphQL, clients can define queries to fetch specific data, mutations to modify data, and data types to describe the structure of the data exchanged between clients and servers. It offers a flexible and efficient approach to data retrieval where clients can specify the exact data they need, and the server responds with the requested data in a predictable structure. It allows clients to avoid over-fetching or under-fetching data, resulting in more efficient and optimized communication between clients and servers.
  • LDAP (Lightweight Directory Access Protocol) is often used by organizations as a central repository for user information and as an authentication service. It can also be used to store the role information for application users.
  • OpenLDAP is an open-source software implementation of the LDAP protocol.

Let’s get started!

Additional Readings

Introduction

The Book API is a Spring Boot application that uses GraphQL to provide an API. It has 2 queries and 3 mutations, all of which require authentication and authorization for access.

  • The getBooks and getBookById queries do not require any authentication;
  • The createBook, deleteBook and addBookReview mutations requires users authenticated.

To handle authentication and authorization in the Book API, LDAP (OpenLDAP) is used.

Prerequisites

If you would like to follow along, you must have Java 17+ and Docker installed on your machine.

Creating Book API Spring Boot app

Let’s create a Spring-Boot application using Spring Initializr.

The application name will be spring-boot-graphql-ldap-book-api and the dependencies needed are: Spring Web, Spring for GraphQL, Spring Data JPA, H2 Database, Spring Security, and Lombok.

We will use the Spring Boot version 3.1.3 and Java 17. Here is the link that contains all the setup mentioned previously.

Click the GENERATE button to download a zip file. Unzip the file to a preferred folder and then open the spring-boot-graphql-ldap-book-api project in your IDE.

Add LDAP dependencies

Let’s update the pom.xml by adding the following LDAP dependencies (code in bold):

<?xml version="1.0" encoding="UTF-8"?>
<project ...>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>org.springframework.ldap</groupId>
            <artifactId>spring-ldap-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-ldap</artifactId>
        </dependency>
        ...
    </dependencies>
    ...
</project>

Create some packages

In order to keep our code organized, let’s create the following packages inside com.example.springbootgraphqlldapbookapi root package: controller, model, repository, security and service.

Create the Model classes

In model package, let’s create the Review entity classes with the content below:

package com.example.springbootgraphqlldapbookapi.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Data;

@Data
@Entity
public class Review {

    @Id
    @GeneratedValue
    private Long id;

    private String reviewer;
    private String comment;
    private Integer stars;
}

Also, in model package, let’s create the Book entity class with the following content:

package com.example.springbootgraphqlldapbookapi.model;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String title;
    private String author;
    private Integer pages;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Review> reviews = new ArrayList<>();
}

The Review and Book classes are entity classes. They are used to represent tables in the H2 database.

Create the Repository class

In repository package, let’s create the BookRepository class with the content below:

package com.example.springbootgraphqlldapbookapi.repository;

import com.example.springbootgraphqlldapbookapi.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
}

The BookRepository class is an interface that extends the JpaRepository interface. It serves as a repository for managing Book entities. By extending JpaRepository<Book, Long>, it inherits several methods for performing common database operations, such as saving, updating, deleting, and querying Book entities.

Create the Service classes

In service package, let’s create the BookService interface with the content below:

package com.example.springbootgraphqlldapbookapi.service;

import com.example.springbootgraphqlldapbookapi.model.Book;

import java.util.List;

public interface BookService {

    List<Book> getBooks();

    Book validateAndGetBookById(Long id);

    Book addBook(String title, String author, int pages);

    Book deleteBook(Long id);

    Book addBookReview(Long id, String reviewer, String comment, int stars);
}

Also, in service package, let’s create the implementation of the BookService interface:

package com.example.springbootgraphqlldapbookapi.service;

import com.example.springbootgraphqlldapbookapi.model.Book;
import com.example.springbootgraphqlldapbookapi.model.Review;
import com.example.springbootgraphqlldapbookapi.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor
@Service
public class BookServiceImpl implements BookService {

    private final BookRepository bookRepository;

    @Override
    public List<Book> getBooks() {
        return bookRepository.findAll();
    }

    @Override
    public Book validateAndGetBookById(Long id) {
        return bookRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Book with ID %s not found".formatted(id)));
    }

    @Override
    public Book addBook(String title, String author, int pages) {
        Book book = new Book();
        book.setTitle(title);
        book.setAuthor(author);
        book.setPages(pages);
        book = bookRepository.save(book);
        return book;
    }

    @Override
    public Book deleteBook(Long id) {
        Book book = validateAndGetBookById(id);
        bookRepository.delete(book);
        return book;
    }

    @Override
    public Book addBookReview(Long id, String reviewer, String comment, int stars) {
        Book book = validateAndGetBookById(id);
        Review review = new Review();
        review.setReviewer(reviewer);
        review.setComment(comment);
        review.setStars(stars);
        book.getReviews().add(review);
        book = bookRepository.save(book);
        return book;
    }
}

The BookService interface and BookServiceImpl class are part of the service layer. They provide methods for managing books, including retrieving all books, adding a new book, deleting a book, and adding a review to a book.

Create the Security Config Class

In security package, let’s create the WebSecurityConfig class with the following content:

package com.example.springbootgraphqlldapbookapi.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        .requestMatchers("/graphql", "/graphiql").permitAll()
                        .anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults())
                .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(AbstractHttpConfigurer::disable)
                .build();
    }
}

The WebSecurityConfig class is a configuration class responsible for handling security-related configurations. Key points are as follows:

  • It is annotated with @Configuration and @EnableWebSecurity, indicating that it provides security-related configurations and enables web security features.
  • The @EnableMethodSecurity(securedEnabled = true) annotation enables method-level security using the @Secured annotation.
  • The securityFilterChain() method configures the security filters and rules for incoming HTTP requests.
  • The authorizeHttpRequests() method specifies the authorization rules for different endpoints. In this case, “/graphql” and “/graphiql” endpoints are permitted to be accessed without authentication, while any other request requires authentication.
  • The httpBasic() configures the HTTP Basic Authentication using the default settings.
  • The sessionManagement() configures the session management to be stateless, which means no session state will be maintained between requests.
  • The csrf() disables Cross-Site Request Forgery (CSRF) protection in a Spring Security setup.
  • Finally, the build method is called to build the SecurityFilterChain and return it as the result.

Create the LdapAuthentication Config class

Let’s implement the LdapAuthenticationConfig class in the security package, with the following content:

package com.example.springbootgraphqlldapbookapi.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.ldap.LdapBindAuthenticationManagerFactory;
import org.springframework.security.ldap.userdetails.PersonContextMapper;

@Configuration
@EnableWebSecurity
public class LdapAuthenticationConfig {

    @Bean
    public AuthenticationManager ldapAuthenticationManager(BaseLdapPathContextSource contextSource) {
        LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource);
        factory.setUserDnPatterns(USER_DN_PATTERN);
        factory.setUserDetailsContextMapper(new PersonContextMapper());
        return factory.createAuthenticationManager();
    }

    private static final String USER_DN_PATTERN = "uid={0}";
}

In this class, we’re setting up a configuration for bind authentication. This method is widely used to authenticate users with LDAP. In bind authentication, the LDAP server verifies the user’s credentials (username and password) provided. The benefit of bind authentication is that the user’s sensitive information (like the password) remains hidden from clients, adding an extra layer of security against potential leaks.

Create the Inputs and the Controller classes

In the controller package, we will create two records and two classes: BookInput, ReviewInput, BookController, and CallbackController.

Let’s first create the BookInput record with the content below:

package com.example.springbootgraphqlldapbookapi.controller;

public record BookInput(String title, String author, int pages) {
}

Then, let’s create the ReviewInput record with the following content:

package com.example.springbootgraphqlldapbookapi.controller;

public record ReviewInput(String reviewer, String comment, int stars) {
}

Finally, let’s create the BookController class with the content below:

package com.example.springbootgraphqlldapbookapi.controller;

import com.example.springbootgraphqlldapbookapi.model.Book;
import com.example.springbootgraphqlldapbookapi.service.BookService;
import lombok.RequiredArgsConstructor;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;

import java.util.List;

@RequiredArgsConstructor
@Controller
public class BookController {

    private final BookService bookService;

    @QueryMapping
    public List<Book> getBooks() {
        return bookService.getBooks();
    }

    @QueryMapping
    public Book getBookById(@Argument Long bookId) {
        return bookService.validateAndGetBookById(bookId);
    }

    @PreAuthorize("isAuthenticated()")
    @MutationMapping
    public Book createBook(@Argument BookInput bookInput) {
        return bookService.addBook(bookInput.title(), bookInput.author(), bookInput.pages());
    }

    @PreAuthorize("isAuthenticated()")
    @MutationMapping
    public Book deleteBook(@Argument Long bookId) {
        return bookService.deleteBook(bookId);
    }

    @PreAuthorize("isAuthenticated()")
    @MutationMapping
    public Book addBookReview(@Argument Long bookId, @Argument ReviewInput reviewInput) {
        return bookService.addBookReview(bookId, reviewInput.reviewer(), reviewInput.comment(), reviewInput.stars());
    }
}

The BookInput and ReviewInput records serve as input models for the BookController. They represent the input data for creating a new book and adding a review, respectively.

The BookController is responsible for handling requests related to books. It interacts with the BookService to perform operations such as retrieving books, creating a book, deleting a book, and adding a review.

The controller methods are annotated with @QueryMapping and @MutationMapping to specify their mapping to GraphQL queries and mutations. The @PreAuthorize annotation ensures that only authorized users can access the methods.

Create the schema.graphqls

In resources/graphql folder, let’s create the schema.graphqls file with the following content:

# ----
# Book

type Book {
    id: ID!
    title: String!
    author: String!
    pages: Int!
    reviews: [Review!]!
}

input BookInput {
    title: String!
    author: String!
    pages: Int!
}

# ------
# Review

type Review {
    id: ID!
    reviewer: String!
    comment: String!
    stars: Int!
}

input ReviewInput {
    reviewer: String!
    comment: String!
    stars: Int!
}

# ---

type Query {
    getBooks: [Book!]!
    getBookById(bookId: ID!): Book
}

# ---

type Mutation {
    createBook(bookInput: BookInput!): Book!
    deleteBook(bookId: ID!): Book!
    addBookReview(bookId: ID!, reviewInput: ReviewInput): Book!
}

In this file we provide the GraphQL schema for the Book API. It defines two main types: Book and Review. The Book type includes properties such as id, title, author, pages, and reviews. The Review type includes properties like id, reviewer, comment, and stars.

The schema also defines input types: BookInput and ReviewInput, used for creating new books and adding reviews, respectively.

The Query type includes operations for fetching book data, such as getting a list of books or retrieving a specific book by its ID.

The Mutation type includes operations for modifying book data, including creating a new book, deleting a book by its ID, and adding a review to a book.

Update the application.properties

Let’s update the application.properties file with the content below:

spring.application.name=book-api

spring.datasource.url=jdbc:h2:mem:appdb
spring.datasource.username=sa
spring.datasource.password=sa

spring.graphql.graphiql.enabled=true

spring.ldap.urls=ldap://localhost:389
spring.ldap.base=ou=users,dc=mycompany,dc=com
spring.ldap.username=cn=admin,dc=mycompany,dc=com
spring.ldap.password=admin
spring.ldap.anonymousReadOnly=true

logging.level.org.springframework.security=DEBUG

A shorter explanation of the properties:

  • spring.application.name: Specifies the name of the application;
  • spring.datasource.url, spring.datasource.username, spring.datasource.password: Configures the H2 in-memory database connection properties;
  • spring.graphql.graphiql.enabled: Enables the GraphiQL tool, which provides an interactive GraphQL IDE, allowing you to explore and test the API;
  • spring.ldap.urls: Tells where the LDAP server is located;
  • spring.ldap.base: Sets the starting point in the LDAP directory to find user info;
  • spring.ldap.username: Supplies the app’s username to access LDAP;
  • spring.ldap.password: Offers the app’s secret key (password) to access LDAP;
  • spring.ldap.anonymousReadOnly: Lets anyone read LDAP info without a key (anonymous access);
  • logging.level.org.springframework.security: Sets the logging level for Spring Security to DEBUG, allowing for detailed logging of security-related events.

Implementing Tests

Start OpenLDAP

In a terminal, let’s start the OpenLDAP Docker container by running the following command:

docker run --rm --name openldap \
  -p 389:389 \
  -e LDAP_ORGANISATION="MyCompany Inc." \
  -e LDAP_DOMAIN=mycompany.com \
  osixia/openldap:1.5.0

Import OpenLDAP Users

Create the LDIF file

Create a file named ldap-mycompany-com.ldif in the root folder of the Book API application with the following content:

dn: ou=groups,dc=mycompany,dc=com
objectclass: organizationalUnit
objectclass: top
ou: groups

dn: cn=user,ou=groups,dc=mycompany,dc=com
cn: user
gidnumber: 500
objectclass: posixGroup
objectclass: top

dn: ou=users,dc=mycompany,dc=com
objectclass: organizationalUnit
objectclass: top
ou: users

dn: uid=app-user,ou=users,dc=mycompany,dc=com
cn: App User
gidnumber: 500
givenname: App
homedirectory: /home/users/app-user
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: top
sn: User
uid: app-user
uidnumber: 1000
userpassword: {MD5}ICy5YqxZB1uWSwcVLSNLcA==

The OpenLDAP users are defined in the LDIF (LDAP Directory Interchange Format) file. It has already a pre-defined structure for “mycompany.com” that contains a group called “users” and one user called “App User” with username app-user and password 123.

Import the LDIF file

We can use the ldapadd command to import the LDIF file to OpenLDAP. For it, in a terminal and inside the simple-api root folder, run:

ldapadd -x -D "cn=admin,dc=mycompany,dc=com" -w admin -H ldap:// \
  -f ldap-mycompany-com.ldif

We can check the user imported by using ldapsearch:

ldapsearch -x -D "cn=admin,dc=mycompany,dc=com" \
  -w admin -H ldap://localhost:389 \
  -b "ou=users,dc=mycompany,dc=com" \
  -s sub "(uid=*)"

Testing Book GraphQL API

Start Book API

Make sure you are in the root folder of the Book API application in the terminal. Then, run the following command:

./mvnw clean spring-boot:run

Testing using GraphiQL

In a browser, access http://localhost:8080/graphiql. The GraphQL in-browser tool will be display the following page:

The interface is split into two sections. On the left side, we can input queries and mutations, while the right side displays the corresponding results.

To begin, let’s fetch the list of all books. The result should only include the “id” of each book.

{
  getBooks {
    id
  }
}

We should get:

{
  "data": {
    "getBooks": []
  }
}

Let’s create a book. In this command, we provide the necessary details of the book, including “title,” “author,” and “pages,” and specify that we want the “id” of the book to be returned.

mutation {
  createBook(bookInput: {title: "Java 17", author: "Peter", pages: 120}) {
    id
  }
}

After clicking the purple play button, we should get the following as response:

{
  "errors": [
    {
      "message": "Unauthorized",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createBook"
      ],
      "extensions": {
        "classification": "UNAUTHORIZED"
      }
    }
  ],
  "data": null
}

Let’s attempt to create the book again, but this time we’ll provide the app-use credentials.

First, let’s get the base64-encoded representation of the user credentials, “app-user:123”. For it, run the following command in a terminal:

echo -n app-user:123 | base64

It should return:

YXBwLXVzZXI6MTIz

Next, let’s go back to GraphiQL. Click on the Headers tab located at the bottom-left and provide the following JSON:

{
  "Authorization": "Basic YXBwLXVzZXI6MTIz"
}

After that, let’s run the createBook mutation:

mutation {
  createBook(bookInput: {title: "Java 17", author: "Peter", pages: 120}) {
    id
  }
}

We should get:

{
  "data": {
    "createBook": {
      "id": "1"
    }
  }
}

Next, we’ll fetch the list of all books. The result should only include the “title” and “author” of each book.

{
  getBooks {
    title
    author
  }
}

The following should be returned:

{
  "data": {
    "getBooks": [
      {
        "title": "Java 17",
        "author": "Peter"
      }
    ]
  }
}

Let’s create a review for the book with an id of 1, which we previously created.

mutation {
  addBookReview(
    bookId: 1
    reviewInput: {reviewer: "ivan", comment: "good", stars: 4}
  ) {
    id
  }
}

We should get as response:

{
  "data": {
    "addBookReview": {
      "id": "1"
    }
  }
}

Let’s retrieve information about the book with an id of 1. We are interested in the book’s “title,” “author,” and its associated “reviews.” Regarding the reviews, we would like to retrieve their “comment” and “stars” ratings.

{
  getBookById(bookId: 1) {
    title
    author
    reviews {
      comment
      stars
    }
  }
}

The following should be returned:

{
  "data": {
    "getBookById": {
      "title": "Java 17",
      "author": "Peter",
      "reviews": [
        {
          "comment": "good",
          "stars": 4
        }
      ]
    }
  }
}

Now, let’s delete the book whose id is 1:

mutation {
  deleteBook(bookId: 1) {
    id
    title
  }
}

We should get as response:

{
  "data": {
    "deleteBook": {
      "id": "1",
      "title": "Java 17"
    }
  }
}

Testing using cURL

Open a terminal and let’s run some cURL calls to Book GraphQL API. Let’s retrieve all books, getting the id of each book:

curl -i -g -X POST \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ getBooks { id } }" }' \
  http://localhost:8080/graphql

It should return:

HTTP/1.1 200
...
{"data":{"getBooks":[]}}

Let’s try to create a book:

curl -i -g -X POST \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { createBook ( bookInput: { title: \"GraphQL\", author: \"Monika\", pages: 12 } ) { id } }" }' \
  http://localhost:8080/graphql

We should get the following “Unauthorized” message:

HTTP/1.1 200
...
{"errors":[{"message":"Unauthorized","locations":[{"line":1,"column":12}],"path":["createBook"],"extensions":{"classification":"UNAUTHORIZED"}}],"data":null}

Alright, let’s provide now a valid user credentials:

curl -i -g -X POST \
  -u app-user:123 \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { createBook ( bookInput: { title: \"GraphQL\", author: \"Monika\", pages: 12 } ) { id } }" }' \
  http://localhost:8080/graphql

We should receive as response:

HTTP/1.1 200
...
{"data":{"createBook":{"id":"2"}}}

Let’s retrieve all books, getting the title and the author (btw, we do not need to provide the user credentials):

curl -i -g -X POST \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ getBooks { title author } }" }' \
  http://localhost:8080/graphql

It should return:

HTTP/1.1 200
...
{"data":{"getBooks":[{"title":"GraphQL","author":"Monika"}]}}

Now, let’s add a review for the book with id 2 that we created earlier:

curl -i -g -X POST \
  -u app-user:123 \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { addBookReview ( bookId: 2, reviewInput: { reviewer: \"ivan\", comment: \"good\", stars: 4 }) { id } }" }' \
  http://localhost:8080/graphql

The response should be:

HTTP/1.1 200
...
{"data":{"addBookReview":{"id":"2"}}}

Now, let’s retrieve information about the book with id 2 (we do not need to provide the user credentials for this request as well). We are interested in the book’s “title” and “author” as well as its “reviews”. The reviews should include the “comment” and “stars” for each review:

curl -i -g -X POST \
  -H "Content-Type: application/json" \
  -d '{ "query": "{ getBookById ( bookId: 2) { title author reviews { comment stars } } }" }' \
  http://localhost:8080/graphql

We should get as response:

HTTP/1.1 200
...
{"data":{"getBookById":{"title":"GraphQL","author":"Monika","reviews":[{"comment":"good","stars":4}]}}}

Next, we’ll proceed to delete the book with id 2:

curl -i -g -X POST \
  -u app-user:123 \
  -H "Content-Type: application/json" \
  -d '{ "query": "mutation { deleteBook ( bookId: 2) { id } }" }' \
  http://localhost:8080/graphql

We should get as response:

HTTP/1.1 200
...
{"data":{"deleteBook":{"id":"2"}}}

Shutdown

In the terminal where the Book API is running, press Ctrl+C to stop the application.

Similarly, in the terminal where OpenLDAP is running, repeat the process by pressing Ctrl+C.

Conclusion

In this article, we demonstrated the implementation of a Book API using Spring Boot, GraphQL, and OpenLDAP. By leveraging these technologies, developers can build secure and efficient APIs with fine-grained access control. The integration of Spring Boot and GraphQL provided a flexible approach to data retrieval, while OpenLDAP simplified the authentication and authorization process.

Support and Engagement

If you enjoyed this article and would like to show your support, please consider taking the following actions:

  • 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
  • 🌐 Share my story on Social Media;
  • 🔔 Follow me on: Medium | LinkedIn | Twitter;
  • ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.
Spring Boot
Ldap
GraphQL
Technology
Software Development
Recommended from ReadMedium