REST API Authentication with Spring Security and MongoDB

Hello everyone! Leaving for the weekend, we are sharing with you an article that was translated ahead of the start of the course "Developer on the Spring Framework" .





In previous articles, we created a RESTful web service, now we’ll talk about security

Introduction


In a previous post, we looked at how to create a REST API using the Java Spring Boot and MongoDB frameworks. The API, however, did not require any authentication, which means that it is probably still not ready for use. Therefore, this guide will show you how to use the Spring built-in security environment to add an authentication level to this API.

Why does our API need authentication?


The APIs provide a simple interface for interacting with internal data, so it makes sense that you do not want anyone to have access to this data and change it. Authentication ensures that only trustworthy users can access the API.

How it works


We will use basic HTTP authentication, which uses a username and password. The username and password are separated on one line by a colon in the following format username:password.

This line is then encoded using Base64 encoding , so the line admin:p@55w0Rdwill be encoded in the next line YWRtaW46cEA1NXcwUmQ=(although I would suggest using a stronger password than “p @ 55w0Rd”). We can attach this authentication to our requests by adding a header Authentication. This heading for the previous example would look like this (where “Basic” means the password uses basic HTTP authentication):

Authentication: Basic YWRtaW46cEA1NXcwUmQ=

How Spring Manages Security


Spring offers an add-in called Spring Security , which makes authentication highly customizable and extremely simple. We can even use some of the skills that we learned in a previous post when setting up!

What do we need


  • A new collection in our MongoDB instance called "users"
  • A new document in the users collection with the following fields (any other fields are optional, but these are necessary): username, password (hashed using the BCrypt algorithm, more on that later)
  • Sources from the previous post

BCrypt for password hashing


Hashing is a one-way encryption algorithm. In fact, after hashing, it’s almost impossible to discover what the original data looked like. The BCrypt hash algorithm first salts a piece of text and then hashes it to a string of 60 characters. The Java BCrypt encoder offers a method matchesthat checks if a string matches a hash. For example, a password p@55w0Rdthat is hashed with BCrypt may have a meaning $2b$10$Qrc6rGzIGaHpbgPM5kVXdeNZ9NiyRWC69Wk/17mttHKnDR2lW49KS. When calling the matchesBCrypt method for an unencrypted and hashed password, we get a value true. These hashes can be generated using the BCrypt encoder built into Spring Security.

Why should we hash passwords?


We have all heard of recent cyber attacks that resulted in stolen passwords from large companies. So why is it only recommended to change our passwords after hacking? Because these large companies made sure that passwords are always hashed in their databases!

Although it’s always worth changing passwords after such data hacks, password hashing makes it extremely difficult to find the user's real password, since it is a one-way algorithm. In fact, it may take years to crack a complex password hashed properly. This provides an additional level of protection against password theft. And Spring Security simplifies hashing, so the real question should be: “Why not?”

Adding a user to MongoDB


I will add a minimum of fields necessary for my collection users(users), so a document with users in my database will contain only username(username) and hashed BCrypt password(password). In this example, my username will be admin, and my password will be welcome1, but I would suggest using a more robust username and password in the production level API.

db.users.insert({
  “username” : “admin”,
  “password” : “$2a$10$AjHGc4x3Nez/p4ZpvFDWeO6FGxee/cVqj5KHHnHfuLnIOzC5ag4fm”
});

These are all the settings needed in MongoDB! The rest of the configuration will be done in our Java code.

Adding a user model and repository


The previous post described in detail the models and repositories of Mongo, so I will not go into details about how they work here - if you want to refresh your knowledge, do not hesitate to visit my previous post!

The downside is that Spring needs to know what the document user(model) will look like and how to access the collection userin the database (repositories). We can put these files in the same folders of models and repositories, respectively, as we did in the previous exercise.

Model


The model will be a Java base class with custom _id, usernameand password. The file will be named Users.java. and will look like this:

package com.example.gtommee.rest_tutorial.models;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;

public class Users {
  @Id
  public ObjectId _id;

  public String username;
  public String password;

  public Users() {}

  public Users(ObjectId _id, String username, String password) 
{
    this._id = _id;
    this.username = username;
    this.password = password;
  }

  public void set_id(ObjectId _id) { this._id = _id; }

  public String get_id() { return this._id.toHexString(); }

  public void setPassword(String password) { this.password = password; }

  public String getPassword() { return password; }

  public void setUsername(String username) { this.username = username; }

  public String getUsername() { return username; }
}

Repository


The repository will be called UsersRepository.javaand will look like this - remember, we will need to find users by them username, so we will need to include the method findByUsernamein the repository interface.

package com.example.gtommee.rest_tutorial.repositories;

import com.example.gtommee.rest_tutorial.models.Users;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UsersRepository extends MongoRepository<Users, String> {
  Users findByUsername(String username);
}

And that's it for the model and the repository!

Adding Security Dependencies


There should be a file with a name in the root directory of the project pom.xml. We have not touched this file yet, but the pom file contains all the dependencies of our project, and we are going to add a couple of them, so let's start by opening this file and scrolling down to the tag . The only new dependency we need is spring-starter-security . Spring has a built-in version manager, so the dependency we have to add to the tag is as follows:<dependencies>

<dependencies>

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

And Maven will download the source files for us, so our dependencies must be ready to go!

Create Authentication Service


We need to tell Spring where our user data is and where to find the information needed for authentication. To do this, we can create an authentication service (Authentication Service). Let's start by creating a new folder in src/main/resources/java/[package name]called services, and we can create a new file in this configuration folder with the name MongoUserDetailsService.java.

MongoUserDetailsService.java


This class has one main component, so I’ll just give the whole class here and then explain it below:

package com.example.gtommee.rest_tutorial.services;

import com.example.gtommee.rest_tutorial.models.Users;
import com.example.gtommee.rest_tutorial.repositories.UsersRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class MongoUserDetailsService implements UserDetailsService{
  @Autowired
  private UsersRepository repository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Users user = repository.findByUsername(username);

    if(user == null) {
      throw new UsernameNotFoundException(“User not found”);
    }

    List<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(“user”));

    return new User(user.getUsername(), user.getPassword(), authorities);
  }
}

This snippet begins with the imports we need in the file. Further, the section implements UserDetailsServiceindicates that this class will create a service for searching and authenticating users. Then, the annotation @Componentindicates that this class can be embedded in another file (for example, the SecurityConfiguration file, which we will go through several sections).

Annotation @Autowiredover private UsersRepository repository; is an example of implementation, this property provides us with an instance of ours UsersRepositoryfor work. Annotation @Overrideindicates that this method will be used instead of the default UserDetailsService method. First, this method gets the object Usersfrom the MongoDB data source using the method findByUsernamethat we declared in UsersRepository.

The method then checks whether the user was found or not. Then the user is granted permissions / role (this can add additional authentication levels for access levels, but one role will be enough for this lesson). Finally, the method returns a Spring object Userwith username, passwordand the roleauthenticated user.

Create Security Configuration


We will need to redefine some of Spring’s built-in security protocols to use our database and hash algorithm, so we need a special configuration file. To create it, we must create a new folder in src/main/resources/java/[package name]with the name config, and we also need to create a new file in this configuration folder with the name SecurityConfiguration.java. This file has several important parts, so let's start with the SecurityConfiguration base class:

SecurityConfiguration.java


package com.example.gtommee.rest_tutorial.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;
}

There is already enough to deal with, so let's start from above. An annotation @Configurationindicates that the class will contain Java beans, described in detail here. An annotation @EnableConfigurationPropertiesindicates what the class will contain as a special configuration bean. Then the instruction will extends WebSecurityConfigurerAdaptermap the parent class WebSecurityConfigurerAdapterto our configuration class , providing our class with everything necessary to ensure that its security rules are respected. Finally, the class will automatically inject an instance ( @Autowired) MongoUserDetailsService, which we can use later in this file.

Authentication step


Next, we need to tell Spring Security how we want to handle user authentication. By default, Spring Security has a predefined username and password, CSRF Protection, and session management . However, we want our users to use their username and password to access the database. In addition, since our users will be re-authenticated at each request, rather than logging in, we do not need CSRF Protection and session management, so we can add a method with a name configurethat overrides the default authentication scheme to tell Spring exactly how we want to handle authentication, and will look like this:

@Override
protected void configure(HttpSecurity http) throws Exception {
 http
   .csrf().disable()
   .authorizeRequests().anyRequest().authenticated()
   .and().httpBasic()
   .and().sessionManagement().disable();
}

Again, quite a lot of things are happening here, so we will sort it out in stages. The annotation @Overridetells Spring Boot to use the method configure (HttpSecurity http)instead of the default Spring configuration. Then we call a series of methods for the object httpwhere the actual configuration takes place. These methods do the following:

  • csrf().disable(): Disables CSRF Protection because it is not needed for the API
  • authorizeRequests().anyRequest().authenticated(): Declares that all requests to any endpoint must be authorized, otherwise they must be rejected.
  • and().httpBasic(): Springso that it expects basic HTTP authentication (discussed above).
  • .and().sessionManagement().disable(): tells Spring not to store session information for users, as this is not necessary for the API

Adding a Bcrypt Encoder


Now we need to tell Spring to use the BCrypt encoder for hashing and comparing passwords - this sounds like a difficult task, but in fact it is very simple. We can add this encoder by simply adding the following lines to our class SecurityConfiguration:

@Bean
public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
}

And all the work! This simple bean tells Spring that the PasswordEncoder we want to use is Spring Boot BCryptPasswordEncoder()to encode and compare password hashes. Spring Boot also includes several other password encoders - I recommend trying them if you want to experiment!

Specify Authentication Manager


Finally, we must indicate in our SecurityConfigurationthat we want to use MongoUserDetailsService(which we created in the previous section) for our authentication. We can do this using the following method:

@Override
public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
  builder.userDetailsService(userDetailsService);
}

This method simply overrides the default configuration AuthenticationManagerBuilder, replacing our own custom data transfer service instead.

Final SecurityConfiguration.java file


import com.example.gtommee.rest_tutorial.services.MongoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;

 @Override
 protected void configure(HttpSecurity http) throws Exception {
   http
     .csrf().disable()
     .authorizeRequests().anyRequest().authenticated()
     .and().httpBasic()
     .and().sessionManagement().disable();
 }

 @Bean
 public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
 }

 @Override
 public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
   builder.userDetailsService(userDetailsService);
 }
}

Authentication check


I will test a quick- GETrequest with valid and incorrect authentication to make sure that the configuration works as planned.

Incorrect Username / Password

URL: http://localhost:8080/pets/
Method: GET
Login: Basic YWRtaW46d2VsY29tZQ==

Reply:

401 Unauthorized

Valid Username / Password

URL: http://localhost:8080/pets/
Method: GET
Login: Basic YWRtaW46d2VsY29tZTE=

Reply:

[
 {
   “_id”: “5aeccb0a18365ba07414356c”,
   “name”: “Spot”,
   “species”: “dog”,
   “breed”: “pitbull”
 },
 {
   “_id”: “5aeccb0a18365ba07414356d”,
   “name”: “Daisy”,
   “species”: “cat”,
   “breed”: “calico”
 },
 {
   “_id”: “5aeccb0a18365ba07414356e”,
   “name”: “Bella”,
   “species”: “dog”,
   “breed”: “australian shepard”
 }
]

Conclusion


It works as it should! Basic HTTP authentication for the Spring Boot API can be tricky, but hopefully this guide will help make it more understandable. Authentication is a must in today's cyber climate, so tools like Spring Security are critical to ensure the integrity and security of your data.

That's all! Meet me on the course .

All Articles