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 securityIntroduction
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@55w0Rd
will 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 matches
that checks if a string matches a hash. For example, a password p@55w0Rd
that is hashed with BCrypt may have a meaning $2b$10$Qrc6rGzIGaHpbgPM5kVXdeNZ9NiyRWC69Wk/17mttHKnDR2lW49KS
. When calling the matches
BCrypt 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 user
in 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
, username
and 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.java
and will look like this - remember, we will need to find users by them username
, so we will need to include the method findByUsername
in 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 UserDetailsService
indicates that this class will create a service for searching and authenticating users. Then, the annotation @Component
indicates that this class can be embedded in another file (for example, the SecurityConfiguration file, which we will go through several sections).Annotation @Autowired
over private UsersRepository repository
; is an example of implementation, this property provides us with an instance of ours UsersRepository
for work. Annotation @Override
indicates that this method will be used instead of the default UserDetailsService method. First, this method gets the object Users
from the MongoDB data source using the method findByUsername
that 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 User
with username
, password
and the role
authenticated 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 @Configuration
indicates that the class will contain Java beans, described in detail here. An annotation @EnableConfigurationProperties
indicates what the class will contain as a special configuration bean. Then the instruction will extends WebSecurityConfigurerAdapter
map the parent class WebSecurityConfigurerAdapter
to 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 configure
that 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 @Override
tells 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 http
where the actual configuration takes place. These methods do the following:csrf().disable()
: Disables CSRF Protection because it is not needed for the APIauthorizeRequests().anyRequest().authenticated()
: Declares that all requests to any endpoint must be authorized, otherwise they must be rejected.and().httpBasic(): Spring
so 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 SecurityConfiguration
that 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- GET
request with valid and incorrect authentication to make sure that the configuration works as planned.Incorrect Username / PasswordURL: http://localhost:8080/pets/
Method: GET
Login: Basic YWRtaW46d2VsY29tZQ==
Reply:401 Unauthorized
Valid Username / PasswordURL: 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 .