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

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Composite Requests in Salesforce Are a Great Idea
  • Leveraging Salesforce Using Spring Boot
  • What D'Hack Is DPoP?
  • Building REST API Backend Easily With Ballerina Language

Trending

  • Navigating and Modernizing Legacy Codebases: A Developer's Guide to AI-Assisted Code Understanding
  • The Role of AI in Identity and Access Management for Organizations
  • Navigating Change Management: A Guide for Engineers
  • Analyzing Techniques to Provision Access via IDAM Models During Emergency and Disaster Response
  1. DZone
  2. Data Engineering
  3. Databases
  4. Two-Factor Authentication in Spring Webflux REST API

Two-Factor Authentication in Spring Webflux REST API

By 
Yuri Mednikov user avatar
Yuri Mednikov
·
May. 11, 20 · Tutorial
Likes (5)
Comment
Save
Tweet
Share
12.2K Views

Join the DZone community and get the full member experience.

Join For Free

Multi-factor authentication became common practice for many cases, especially for enterprise applications or those that deal with sensitive data (like finance apps). Moreover, MFA is enforced (especially in the EU) by law in a growing number of industries, and if you are working on an app, that by some requirement, has to enable two-factor auth in some way, don’t hesitate to check out this post.

In this article, I will show you how to write a two-factor authentication for a reactive API, built with Spring Webflux. This app uses TOTP (one-time codes, generated by an app on the user device – like Google Authenticator)m as the second security factor, alongside email and password pairs.

How Two-Factor Authentication Does Works

Technically, two-factor authentication (or multi-factor authentication) stands for a security process, where users have to provide 2 or more factors to verify themselves. That means, that usually, a user supplies a password in addition to another identifier. It can be a one-time password, hardware tokens, biometric factors (like fingerprint), etc.

Basically, such practice requires several steps:

  1. User enters email (username) and password.
  2. Alongside credentials, user submits a one-time code, generated by an authenticator app.
  3. The app authenticates email (username) and password and verifies the one-time code, using user’s secret key, issued during the signup process,

That means, that usage of authenticator apps (like Google Authenticator, Microsoft Authenticator, FreeOTP etc) brings a number of advantages, compared to using SMS for code delivery. They are not sensitive for SIM attacks and work without cell/internet connection.

An Example Application

Through this post, we will complete a small simple REST API, which uses two-factor authentication techniques. It requires users to provide both an email-password pair and a short code, generated by an app. You can use any compatible app to generate TOTP; I use Google Authenticator for Android. The source code is available in this github repository. The app requires JDK 11, Maven and MongoDB (to store user profiles). Take a look on the project structure:

The structure of the sample application

Structure of the sample application

I will not go through each component; rather we will concentrate only on AuthService (and implementation), TokenManager (and implementation), and TotpManager (and implementation). These parts are responsible for authentication flows. Each of them provides the following functionality:

  • AuthService – is a component that stores all business logic, related to authentication/authorization, including signup, login, and token validation
  • TokenManager – this component abstracts a code to generate and validate JWT tokens. This makes main business logic implementation-independent from concrete JWT libraries. In this post, I use Nimbus JOSE-JWT.
  • TotpManager – another abstraction to isolate implementation from base logic. It is used to generate a user’s secret and to assert supplied short codes. I do this with this TOTP Java library, but there are other choices as well.

As you can note, I will focus only on auth components. We will start from the user creation process (signup), which requires the secret’s generation and the issue of the token. Then, we will get into the login flow, which involves an assertion of the short-code, supplied by the user.

Implement a Signup Flow

In this section we will complete a signup process that involves the following steps:

  • Get signup request from the client.
  • Check that user does not exist already.
  • Hash password.
  • Generate a secret key.
  • Store users in the database.
  • Issue JWT.
  • Return a response with the user id, secret key, and token.

I separated the main business logic (AuthServiceImpl) from token generation and secret key generation.

General Steps

The main component, AuthServiceImpl, accepts SignupRequest and returns SignupResponse. Behind the scenes, it is responsible for whole signup logic. First, take a look at its implementation:

Java
xxxxxxxxxx
1
47
 
1
@Override
2
public Mono<SignupResponse> signup(SignupRequest request) {
3

          
4
    // generating a new user entity params
5
    // step 1
6
    String email = request.getEmail().trim().toLowerCase();
7
    String password = request.getPassword();
8
    String salt = BCrypt.gensalt();
9
    String hash = BCrypt.hashpw(password, salt);
10
    String secret = totpManager.generateSecret();
11

          
12
    User user = new User(null, email, hash, salt, secret);
13

          
14
    // preparing a Mono
15
    Mono<SignupResponse> response = repository.findByEmail(email)
16
            .defaultIfEmpty(user) // step 2
17
            .flatMap(result -> {
18
                // assert, that user does not exist
19
                // step 3
20
                if (result.getUserId() == null) {
21
                    // step 4
22
                    return repository.save(result).flatMap(result2 -> {
23
                        // prepare token
24
                        // step 5
25
                        String userId = result2.getUserId();
26
                        String token = tokenManager.issueToken(userId);
27
                        SignupResponse signupResponse = new SignupResponse();
28

          
29
                        signupResponse.setUserId(userId);
30
                        signupResponse.setSecretKey(secret);
31
                        signupResponse.setToken(token);
32
                        signupResponse.setSuccess(true);
33
                      
34
                        return Mono.just(signupResponse);
35
                    });
36
                } else {
37
                    // step 6
38
                    // scenario - user already exists
39
                    SignupResponse signupResponse = new SignupResponse();
40
                    signupResponse.setSuccess(false);
41
                  
42
                    return Mono.just(signupResponse);
43
                }
44
            });
45
  
46
    return response;
47
}


Now, let's go step-by-step through my implementation. Basically, we can get two possible scenarios with the signup process: user is new, and we sign it up, or the user is already present in the database, so we have to reject the request.

Consider these steps:

  1. We create a new user entity from the request data and generate a secret.
  2. Provide the new entity as default, if user does not exist.
  3. Check, the repository’s call result.
  4. Save user in the database and obtain a userId
  5. Issue a JWT.
  6. Return a rejecting response if user does already exist.

Note, that here I use jBcrypt library in order to generate secure hashes and salts. This is a more acceptable practice than using SHA functions: please, don’t use them in order to produce hashes, as they are known for vulnerabilities and security issues. I can advise you to check this tutorial on using jBcrypt to get more information.

Generate a Secret Key

Next, we need to implement a function to generate a new secret key. It is abstracted inside TotpManager.generateSecret(). Take a look at the code below:

Java
xxxxxxxxxx
1
 
1
@Override
2
public String generateSecret() {
3
    SecretGenerator generator = new DefaultSecretGenerator();
4

          
5
    return generator.generate();
6
}

Testing

After the signup logic was implemented, we can validate that everything works as expected. First, let's call the signup endpoint to create a new user. The result object contains userId, token, and secret key that you need to add to the app generator (Google Authenticator):

Successful signup

Successful signup

However, we should not be allowed to signup twice with same email. Let try this case to assert, that app actually looks for the existed email before creating a new user:

Login response object

Login response object

The next section deals with a login flow.

Login

The login process consists of two main parts: validating email-password credentials and validating one-time code, supplied by the user. As in the previous section, I start with presenting the required steps. For login it would be:

  • Get login request from the client.
  • Find the user in the database.
  • Asserting existing password with the password supplied in the request.
  • Asserting one-time code.
  • Return login response with token.

The process of JWT generation is the same as in the signup stage.

General Steps

Main business logic is implemented in AuthServiceImpl.login. It does the majority of work. First, we need to find a user by request email in database; otherwise, we provided a default value with null fields. The condition user.getUserId() == null means that the user does not exist, and signup flow should be aborted.

Next, we need to assert that the passwords match. As we stored the password hash in the database, we first need to hash a password from the request with the stored salt and then assert both values.

If passwords matched, we need to verify a code using the secret value, which we stored before. The successful result of validation is followed by the generation of JWT and the creation of the LoginResponse object. The final source code for this part is presented to you below:

Java
xxxxxxxxxx
1
56
 
1
@Override
2
public Mono<LoginResponse> login(LoginRequest request) {
3
    String email = request.getEmail().trim().toLowerCase();
4
    String password = request.getPassword();
5
    String code = request.getCode();
6

          
7
    Mono<LoginResponse> response = repository.findByEmail(email)
8
    // step 1
9
            .defaultIfEmpty(new User())
10
            .flatMap(user -> {
11
                // step 2
12
                if (user.getUserId() == null) {
13
                    // no user
14
                    LoginResponse loginResponse = new LoginResponse();
15
                    loginResponse.setSuccess(false);
16
                  
17
                    return Mono.just(loginResponse);
18
                } else {
19
                    // step 3
20
                    // user exists
21
                    String salt = user.getSalt();
22
                    String secret = user.getSecretKey();
23
                    boolean passwordMatch = BCrypt.hashpw(password, salt).equalsIgnoreCase(user.getHash());
24

          
25
                    if (passwordMatch) {
26
                        // step 4
27
                        // password matched
28
                        boolean codeMatched = totpManager.validateCode(code, secret);
29

          
30
                        if (codeMatched) {
31
                            // step 5
32
                            String token = tokenManager.issueToken(user.getUserId());
33

          
34
                            LoginResponse loginResponse = new LoginResponse();
35
                            loginResponse.setSuccess(true);
36
                            loginResponse.setToken(token);
37
                            loginResponse.setUserId(user.getUserId());
38
                          
39
                            return Mono.just(loginResponse);
40
                        } else {
41
                            LoginResponse loginResponse = new LoginResponse();
42
                            loginResponse.setSuccess(false);
43

          
44
                            return Mono.just(loginResponse);
45
                        }
46
                    } else {
47
                        LoginResponse loginResponse = new LoginResponse();
48
                        loginResponse.setSuccess(false);
49
                      
50
                        return Mono.just(loginResponse);
51
                    }
52
                }
53
            });
54
  
55
    return response;
56
}


So, let's observe the steps:

  1. Provide a default user entity with null fields.
  2. Check that user does exist.
  3. Produce a hash of password from request and salt, stored in a database.
  4. Assert that passwords do match.
  5. Validating one-time code and issue JWT.

Asserting One-Time Codes

To validate one-time codes generated by apps, we have to provide for TOTP library both code and secret, that we saved as a part of the user entity. Take a look at the implementation:

Java
xxxxxxxxxx
1
 
1
@Override
2
public boolean validateCode(String code, String secret) {
3
    TimeProvider timeProvider = new SystemTimeProvider();
4
    CodeGenerator codeGenerator = new DefaultCodeGenerator();
5
    CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
6

          
7
    return verifier.isValidCode(secret, code);
8
}


Testing

Finally, we can do testing to verify that the login process works as we planned. Let 's call the login endpoint with login request payload that contains a generated code from Google Authenticator.

Graph 4. Successful login

An another case to check is the wrong password. No matter if we have correct code or not, the process should be terminated on the password assertion stage:

Login denied because of wrong password

Login denied because of wrong password

That is all for this post. We have created a simple REST API to provide two-factor authentication with TOTP for Spring Webflux. As I mentioned already, we have omitted everything in order to concentrate only on auth logic. That means you can find a full code for this article in this github repository. 

References

  • Dhiraj Ray Password Encryption and Decryption Using jBCrypt (2017) DZone access here.
  • Sanjay Patel Using Nimbus JOSE + JWT in Spring Applications — Why and How (2018) Natural Programmer Blog access here.
  • Scott Brady Creating Signed JWTs using Nimbus JOSE + JWT (2019) access here.
API REST Web Protocols authentication Database app Spring Framework JWT (JSON Web Token) Requests Business logic

Published at DZone with permission of Yuri Mednikov. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Composite Requests in Salesforce Are a Great Idea
  • Leveraging Salesforce Using Spring Boot
  • What D'Hack Is DPoP?
  • Building REST API Backend Easily With Ballerina Language

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!