Two Factor Authentication with Spring Security

In this article of spring security tutorials, we will look at the two factor authentication with Spring security. We are going to use the soft token with Spring Security.

Two Factor Authentication with Spring Security

It’s always a good practice to add some additional layer of security for your application, especially with every changing security dynamics. For some application, having a two factor authentication is a requirement. Though there is no build in two factor authentication with Spring security, however the flexible Spring Security Architecture makes it really easy to add this functionality for your application.

Simply, two factor authentication, referred to as two-step verification or dual-factor authentication, is a security process in which users provide two different authentication factors to verify themselves. In this article, we will change the login process where the customer has to supply an additional security token besides the login credentials. This additional security token will be a onetime password verification code based on Time-based One-time Password TOTP algorithm.

We will use Google Authenticator or similar app to generate this verification code.

1. Application Configuration – Maven

We will use our existing application to build this additional feature. You can download the working code from our GitHub repository. To use the Google Authentication for generating the verification code(s), we need following additional changes in our application.

  1. Generate a secret key during user registration and store it with a user profile.
  2. Provide the secret key / QR-code to the customer post registration. The Google Authentication app will use this QR code to generate the Time-based One-time Password.
  3. Last, we need to change the login process to pass this additional token to Spring Security authentication provider and verify the token entered by the user using the secret key.

As I mentioned earlier, Spring security does not have any build in mechanism to generate and verify this security token and we will use as additional library to help us. We will add the following dependency to our pom.xml file.

<dependency>
   <groupId>dev.samstevens.totp</groupId>
   <artifactId>totp</artifactId>
   <version>1.7.1</version>
</dependency>

There are other similar libraries to generate and verify these Time-based One-time Password. You can use any other library of your choice as the core concept of integrating it with Spring security will remain same.

2. User Entity Change

The next change is the user entity. We need to store the secret key in the user profile during registration. This key is important, as we will use it during the login process to validate the token. We can also introduce a flag to show if the 2-factor-authentication is active for a customer or not. This is how our changed user entity look like:

@Entity
@Table(name = "user")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    @Column(unique = true)
    private String email;
    private String password;
    private String token;
    private boolean accountVerified;
    private int failedLoginAttempts;
    private boolean loginDisabled;
    private boolean mfaEnabled; // flag to indicate of mfa is active for profile
    private String secret; // secret store for the profile, this will be used during the login process.

    //getter and setters
}

3. Customer Registration

The next step for enabling two factor authentication with Spring security is to change the flow of our registration process. In successful registration, we want to do the following additional steps:

  1. Show success message to the customer.
  2. Show the QR code to the customer so they can use app to scan and store the QR code for code generation.

3.1. MfaTokenManager

To store the secret and show the QR code to the customer, we will introduce a new service as MfaTokenManager, which will help us with the following features:

  1. Generate secret key for a customer during registration.
  2. Generate QR code.
  3. Verify the code entered by the customer during login process.

This is how our service look like:

public interface MFATokenManager {
    String generateSecretKey();
    String getQRCode(final String secret) throws QrGenerationException;
    boolean verifyTotp(final String code, final String secret);
}
package com.javadevjournal.core.security.mfa;

import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.HashingAlgorithm;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.util.Utils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service("mfaTokenManager")
public class DefaultMFATokenManager implements MFATokenManager {

    @Resource
    private SecretGenerator secretGenerator;

    @Resource
    private QrGenerator qrGenerator;

    @Resource
    private CodeVerifier codeVerifier;

    @Override
    public String generateSecretKey() {
        return secretGenerator.generate();
    }

    @Override
    public String getQRCode(String secret) throws QrGenerationException {
        QrData data = new QrData.Builder().label("MFA")
            .secret(secret)
            .issuer("Java Development Journal")
            .algorithm(HashingAlgorithm.SHA256)
            .digits(6)
            .period(30)
            .build();
        return Utils.getDataUriForImage(
            qrGenerator.generate(data),
            qrGenerator.getImageMimeType()
        );
    }

    @Override
    public boolean verifyTotp(String code, String secret) {
        return codeVerifier.isValidCode(secret, code);
    }
}

Most of the code is part of the library we are using for our application, however, the code is self explanatory. While generating the QR code, we need the secret key which will be generated by the same service during user registration process.

3.2 User Service

Our UserService creates the user record in the system during the registration process. We will also do some modification to our UserService to perform the following work:

  • Generate the secret and store it with user profile.
  • Generate QR code on successful user registration.

Here is the changed version of our UserService.

@Override
public void register(UserData user) throws UserAlreadyExistException {
    if (checkIfUserExist(user.getEmail())) {
        throw new UserAlreadyExistException("User already exists for this email");
    }
    //some additional work
    userEntity.setSecret(mfaTokenManager.generateSecretKey()); //generating the secret and store with profile
    userRepository.save(userEntity);
}

Another method is to generate the QR code once the registration is complete.

@Override
public MfaTokenData mfaSetup(String email) throws UnkownIdentifierException, QrGenerationException {
    UserEntity user = userRepository.findByEmail(email);
    if (user == null) {
        // we will ignore in case account is not verified or account does not exists
        throw new UnkownIdentifierException("unable to find account or account is not active");
    }
    return new MfaTokenData(mfaTokenManager.getQRCode(user.getSecret()), user.getSecret());
}

Finally our MfaTokenData bean contains the QR code image link and the mfaCode. The mfaCode is important as it will give customer option to manually enter the key in the Authentication App in case they cannot scan the QR code.

public class MfaTokenData implements Serializable {

    private String qrCode;
    private String mfaCode;
    //getter and setter
}

3.3. Registration Controller

The other part is the RegistrationController. In this controller, we will generate and show the QR code to the customer on successful registration.

@PostMapping
public String userRegistration(final @Valid UserData userData, final BindingResult bindingResult, final Model model) {

    try {
        userService.register(userData);
        MfaTokenData mfaData = userService.mfaSetup(userData.getEmail()); //setup QR code post registration
        model.addAttribute("qrCode", mfaData.getQrCode());
        model.addAttribute("qrCodeKey", mfaData.getMfaCode());
        model.addAttribute("qrCodeSetup", true);
    } catch (UserAlreadyExistException | QrGenerationException | UnkownIdentifierException e) {
        //error handing 
        return "account/register";
    }
    //handle message
    return "account/register";
}

3.4. Registration Page

The last part is to show the QR code to our customer on successful registration. We will add the following additional code to our Thymeleaf template.

<img class="col-md-12" th:src="${qrCode}" />
<p th:text="${qrCodeKey}"></p>

You can check and download the complete source code on our GitHub repository.

3.5. Displaying the QR Code

To complete the registration for our two factor authentication with Spring security, we need to display the code to the customer. If you run the application and do a new registration, you will see the following screen on the successful registration.

Two Factor Authentication with Spring Security

You can customize the view as per your requirement or if you like can give option to the customer to enable the two factor authentication with Spring security from profile section. We are only focusing on the integration part.

4. Passing Extra Login Fields with Spring Security

The first part of our application is complete. Now with login process, we want the user to provide the TOTP during the login process for validation. To handle this two factor authentication with spring security, we need to this additional field. I have already covered few options in my other article on passing as additional field with Spring security login page but we will use a different approach for this article as this approach is more suitable for the work we are doing.

For this article, we will extend the AuthenticationDetailsSource and WebAuthenticationDetails. If you remember from my previous article, Spring security use the UsernamePasswordAuthenticationFilter for form-based login to set up and pass the form parameters to the authentication manager. Here is a snippet from the UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

If we go in the setDetails() method

protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
    authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

The AuthenticationDetailsSource builds the auth details from the HttpServletRequest. Since we are working on a web application, we will extend the WebAuthenticationDetails and will tell spring security to use this customer class. This is how our AuthenticationDetailsSource and WebAuthenticationDetails classes look like:

package com.javadevjournal.core.security.web.authentication;

import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;
import java.util.Objects;

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

    private String token;
    /**
     * Records the remote address and will also set the session Id if a session already
     * exists (it won't create one).
     *
     * @param request that the authentication request was received from
     */
    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        this.token = request.getParameter("jdjCustomToken");
    }

    //override toString(), hashCode() and equals
}

@Component
public class CustomWebAuthenticationDetailsSource implements AuthenticationDetailsSource <HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public CustomWebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new CustomWebAuthenticationDetails(context);
    }
}

The code is quite self explanatory.

  1. We call the super to let WebAuthenticationDetails to compete require setup.
  2. Retrieve the customer request parameter and setting this in the token.
  3. In CustomWebAuthenticationDetailsSource class, we initializing and returning our custom AuthenticationDetails class

The last part if to tell Spring Security to use our custom CustomWebAuthenticationDetailsSource and not the default implementation. To do this, we will configure the authenticationDetailsSource in our Spring security configuration class.

@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private CustomWebAuthenticationDetailsSource authenticationDetailsSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login", "/register", "/home")
            .permitAll()
            .antMatchers("/account/**").hasAnyAuthority("CUSTOMER", "ADMIN")
            .and()
            ......

            .formLogin()
            .authenticationDetailsSource(authenticationDetailsSource) //custom authenitcation source
            ....
    }
}

4.1. Adding Extra Parameter to Login Page

Finally, the last part is to add the extra parameter to the login page. This is a simple text field that can be added to the login.html file.

<div class="input-group mb-3">
   <input type="text" name="jdjCustomToken" class="form-control" placeholder="Token">
   <div class="input-group-append">
      <div class="input-group-text">
         <span class="fas fa-lock"></span>
      </div>
   </div>
</div>

Make sure to name the field name same as used in the CustomWebAuthenticationDetails class

5. Custom Authentication Provider

The last part to enable the two factor authentication with Spring security is to validate the user input token with the API and reject any login attempt with invalid and expired token.There are multiple options to do that but the best and recommended way is to create a custom authentication provider and validate the token in the authentication provider.

Since we will do our work using the DAOAuthenticationProvider, we will extend this provider to accomplish our work. We will use the default provider to do most of the standard work but will override the additionalAuthenticationChecks() method to perform the two factor authentication with Spring security.

package com.javadevjournal.core.security.authentication;

@Component
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

    @Resource
    private MFATokenManager mfaTokenManager;

    @Autowired
    public CustomAuthenticationProvider(UserDetailsService userDetailsService) {
        super.setUserDetailsService(userDetailsService);
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {

        super.additionalAuthenticationChecks(userDetails, authentication);
        //token check
        CustomWebAuthenticationDetails authenticationDetails = (CustomWebAuthenticationDetails) authentication.getDetails();
        CustomUser user = (CustomUser) userDetails;
        String mfaToken = authenticationDetails.getToken();
        if (!mfaTokenManager.verifyTotp(mfaToken, user.getSecret())) { //chekcing if the user token matching 
            throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
        }
    }
}

Again this part is really simple:

  • The Default DAOAuthenticationProvider will perform the standard work.
  • In case the user is valid, the additionalAuthenticationChecks() method will get the security token from the CustomWebAuthenticationDetails.
  • CustomeUser service will contain the secret created and stored during the user registration.
  • We will be using the mfaTokenManager.verifyTotp() to verify the secret with customer supplied token.
  • In the token is valid, login will be successful. On the invalid token, we are throwing the BadCredentialsException.

We also need to configure the custom authentication provider for the two factor authentication with Spring security. We can do this with our security configuration class.

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(customAuthenticationProvider);
}

6. Test Application

To test if the two factor authentication with Spring security works as expected, run the application and go the login page. You will see an additional field to provide the security token during the registration process.

two factor authentication with Spring security
Security token during login

If you submit with the wrong token, you will get an invalid login error. Once you provide valid login credentials along with a valid token, you will be logged in to the system.

Summary

In this article, we looked at the option to enable the two factor authentication with Spring security. We saw how to configure and use the Time-based One-time Password TOTP algorithm for your application. In the last part, we created a custom authentication provider to integrate the login and token validation process. The Source code for this article is available on our GitHub repository.