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
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
andgetBookById
queries do not require any authentication; - The
createBook
,deleteBook
andaddBookReview
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 theSecurityFilterChain
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 ldap
add 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 ldapsea
rch:
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.