Home | Send Feedback

Adding TOTP as 2nd factor to Spring Security form login

Published: June 21, 2019  •  java, spring, ionic4, javascript

TOTP (Time-based One-Time Password) is a solution that is often added as second factor to a traditional username/password authentication flow, to increase security.

TOTP is an algorithm based on the HOTP (HMAC-based One-time Password), but uses a time-based component instead of a counter.

TOTP and HOTP depend on a secret that two parties share. In this case, the server and the user share the same secret. The secret is a randomly generated token that is usually displayed in Base32 to the user. During the signup process, the server generates the secret, stores it into the database and shows it to the user. The user then types or copies the secret into an authenticator app that supports TOTP.

There are many TOTP apps available as apps for smartphones, as browser extensions and as standalone programs for the desktop. I use the Google Authenticator on an Android device.

During sign in the user enters username, password, and the code that the TOTP authenticator app displays as second factor. The client sends these three values to the server, where username and password run through a verification process. If they are valid, the server calculates the TOTP with the stored secret of this user and compares it with the client side generated TOTP. If both values match, the user is successfully authenticated.

In the following section, we implement a form login authentication with TOTP as second factor with Spring Security and Spring Boot. The client is a web application written with TypeScript, Ionic, and Angular. The demo application uses jOOQ and stores the data into a file-based H2 database.

For the TOTP calculation on the server, I use the aerogear-otp-java library from the JBoss project.

    <dependency>
      <groupId>org.jboss.aerogear</groupId>
      <artifactId>aerogear-otp-java</artifactId>
      <version>1.0.0</version>
    </dependency>

pom.xml

With this library the TOTP calculation and verification can be implemented with very few code:

 Totp totp = new Totp(secret_from_database);
 if (!totp.verify(totp_from_client)) {
     //TOTP don't match
 }

The library also provides a method to generate TOTP secrets, that we are going to use during the sign up workflow:

String newSecret = Base32.random();

You find the complete source code for the server and client application on GitHub:
https://github.com/ralscha/blog2019/tree/master/totp

TOTP and Spring Security

The application stores the users in a H2 database. The demo application uses this table to store user information.

CREATE TABLE app_user (
    id              BIGINT NOT NULL AUTO_INCREMENT,
    username        VARCHAR(255) NOT NULL,
    password_hash   VARCHAR(255),
    secret          VARCHAR(16),
    enabled         BOOLEAN not null,
    PRIMARY KEY(id),
    UNIQUE(username)    
);

V0001__initial.sql

Besides username and password the user record also holds the secret used for calculating the TOTP. This is the secret that is shared between the client and the server.

Next, I had to implement the two interfaces, UserDetailsService and UserDetails. You find the source code for these two implementations here: JooqUserDetailsService and JooqUserDetails. Standard implementations that fetch the user from the database and hold the user information.


To implement the 2FA authentication, we extend the existing form login authentication flow. Verification of username and password are handled by the default implementation, and we need to write code that extracts the TOTP, sent by the client, calculates the TOTP and compares the two values.

To extract the TOTP from the login request, we implement the interface AuthenticationDetailsSource and extend the class WebAuthenticationDetails

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

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

TotpWebAuthenticationDetailsSource.java

TotpWebAuthenticationDetailsSource creates an instance of TotpWebAuthenticationDetails for every incoming request. The class is configured as Spring-managed bean ( @Component). This is important here because Spring will take care of configuring this class automatically into the Spring Security workflow.

TotpWebAuthenticationDetails is responsible for extracting the TOTP from the HTTP request. Username, password, and TOTP are sent as FormData elements in the body of a POST request.

public class TotpWebAuthenticationDetails extends WebAuthenticationDetails {

  private static final long serialVersionUID = 1L;

  private Long totpKey;

  public TotpWebAuthenticationDetails(HttpServletRequest request) {
    super(request);

    String totpKeyString = request.getParameter("totpkey");
    if (StringUtils.hasText(totpKeyString)) {
      try {
        totpKeyString = totpKeyString.replaceAll("\\s+", "");
        this.totpKey = Long.valueOf(totpKeyString);
      }
      catch (NumberFormatException e) {
        this.totpKey = null;
      }
    }
  }

  public Long getTotpKey() {
    return this.totpKey;
  }

}

TotpWebAuthenticationDetails.java


Next, we implement an AuthenticationProvider that is responsible for verifying username, password, and comparing client and server side TOTP. We don't have to implement this interface from scratch; instead, we extend DaoAuthenticationProvider, which is the default form login authentication provider.

We extend the method authenticate and call the code in the superclass first, which takes care of fetching the user from the database, comparing the password and checking if the user is enabled.

The method either returns an Authentication object or throws an exception; thus, we don't need to check for null.

If we get back an instance of an Authentication object from the superclass call, we are sure that the user exists, the password matches, and the user is enabled.

The second factor is an optional feature in this application, so the code first checks if there is a TOTP secret stored for this user. If yes, compute the TOTP with the secret stored in the database, extract the TOTP, sent from the client, and compare the two values (totp.verify).

Throw an exception if the two TOTP don't match. Also, throw an exception if a secret is stored in the database, but there is no TOTP in the login request, to ensure that users opted in for 2nd-factor authentication always have to provide a TOTP.

@Component
public class TotpAuthenticationProvider extends DaoAuthenticationProvider {

  TotpAuthenticationProvider(UserDetailsService userDetailService) {
    setUserDetailsService(userDetailService);
  }

  @Override
  public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {

    Authentication auth = super.authenticate(authentication);

    if (auth.getDetails() instanceof TotpWebAuthenticationDetails
        && auth.getPrincipal() instanceof JooqUserDetails) {
      String secret = ((JooqUserDetails) auth.getPrincipal()).getSecret();

      if (StringUtils.hasText(secret)) {
        Long totpKey = ((TotpWebAuthenticationDetails) auth.getDetails()).getTotpKey();
        if (totpKey != null) {
          Totp totp = new Totp(secret);
          if (!totp.verify(totpKey.toString())) {
            throw new BadCredentialsException("TOTP Code is not valid");
          }
        }
        else {
          throw new BadCredentialsException("TOTP Code is mandatory");
        }

      }
    }

    return auth;
  }

}

TotpAuthenticationProvider.java

Lastly, we have to configure Spring Security. The only TOTP related configuration is to tell Spring Security to use our custom TotpWebAuthenticationDetails bean.
We do that with a call to authenticationDetailsSource() and pass the TotpWebAuthenticationDetailsSource instance managed by Spring as an argument.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private TotpWebAuthenticationDetailsSource totpWebAuthenticationDetailsSource;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // @formatter:off
       http
  .csrf().disable()
    .formLogin()
      .successHandler((request, response, authentication) -> response.setStatus(HttpServletResponse.SC_OK))
      .failureHandler((request, response, exception) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
      .authenticationDetailsSource(this.totpWebAuthenticationDetailsSource)
      .permitAll()
    .and()
      .logout()
        .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
        .deleteCookies("JSESSIONID")
    .and()
      .authorizeRequests()
        .antMatchers("/signup", "/signup-confirm-secret").permitAll()
        .anyRequest().authenticated()
        .and()
          .exceptionHandling()
            .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
    // @formatter:on
  }

SecurityConfig.java

Everything else is autoconfigured. The TotpAuthenticationProvider we created in the step before is automatically registered in Spring Security (notice the @Component annotation on the class).

All the handler configurations are only added because the demo client is a single page web application. The /login and /logout endpoints send, by default, redirects as an HTTP response, which is useful if you are developing a multi-page application. For SPA it's easier if we get back a proper HTTP Status code of 401 and 200 instead of a redirect code (30x).

Demo application

With everything in place, the application should work, and you should be able to sign in. The first time you start the Spring boot application, it inserts three test users into the database.

Username Password Secret
admin admin W4AU5VIXXCPZ3S6T
user user LRVLAZ4WVFOU3JBF
lazy lazy

Install a TOTP authenticator app, create a new entry with the given secret. Enter username, password and a TOTP (except for the lazy user), and you should be able to log in.

login

Sign Up

In this section, we are going to implement a simple sign up workflow with TOTP. Please notice that the code I present here implements a stripped down sign up workflow. In a production application, you should consider adding things like captcha, rate limiting, and an email verification workflow.

In the first dialog, the user enters username and password and chooses if he wants to use 2FA or not. signup1

Click on Sign up sends the information to the server.

The server checks if the username is not already taken and checks for weak passwords, with the passpol library.

  @PostMapping("/signup")
  public SignupResponse signup(@RequestParam("username") @NotEmpty String username,
      @RequestParam("password") @NotEmpty String password,
      @RequestParam("totp") boolean totp) {

    // cancel if the user is already registered
    int count = this.dsl.selectCount().from(APP_USER)
        .where(APP_USER.USERNAME.equalIgnoreCase(username)).fetchOne(0, int.class);
    if (count > 0) {
      return new SignupResponse(
          ch.rasc.totp.security.SignupResponse.Status.USERNAME_TAKEN);
    }

    Status status = this.passwordPolicy.check(password);
    if (status != Status.OK) {
      return new SignupResponse(
          ch.rasc.totp.security.SignupResponse.Status.WEAK_PASSWORD);
    }

AuthController.java

If all checks pass and the user enabled 2FA, a random secret is generated, and the user is inserted into the database. In this case the enabled flag is set to false, so a user that enabled 2nd-factor authentication can't log in just yet.

The generated secret is sent back to the client.

    if (totp) {
      String secret = Base32.random();

      this.dsl
          .insertInto(APP_USER, APP_USER.USERNAME, APP_USER.PASSWORD_HASH,
              APP_USER.ENABLED, APP_USER.SECRET)
          .values(username, this.passwordEncoder.encode(password), false, secret)
          .execute();
      return new SignupResponse(ch.rasc.totp.security.SignupResponse.Status.OK, username,
          secret);
    }

AuthController.java

When the user does not want to use 2FA, the signup method inserts the user and sets the enabled flag to true and ends the sign up process for this user.

    this.dsl
        .insertInto(APP_USER, APP_USER.USERNAME, APP_USER.PASSWORD_HASH, APP_USER.ENABLED,
            APP_USER.SECRET)
        .values(username, this.passwordEncoder.encode(password), true, null).execute();
    return new SignupResponse(ch.rasc.totp.security.SignupResponse.Status.OK);
  }

AuthController.java

Users with 2FA now see the following dialog.

signup2

The randomly generated secret is displayed as a QR code. Most TOTP authenticator apps have a built-in QR code scanner to import the secret into the app. Which is more convenient and less error-prone than typing the secret manually into the app.

The value that is encoded in the QR code must follow this pattern:

otpauth://totp/USERNAME?secret=SECRET&issuer=ISSUER

The most important information is the server generated secret. Issuer and username are imported into the authenticator app, but are not used for the TOTP calculation. But they are useful if you have many different TOTPs, because the app shows this information besides the TOTP and you know which TOTP belongs to which site.

app

See also this wiki page for more information about the otpauth URI:
https://github.com/google/google-authenticator/wiki/Key-Uri-Format

For creating the QR code, I use the qrcode library.

The library can either draw the QR directly onto a canvas or generate a data URL that can be used as value for the src attribute of a img tag, which is the approach I use in this application.

    QRCode.toDataURL(link).then(url => this.qrCode = url);

signup-secret.page.ts

    <a [href]="qrSafeLink"><img [src]="qrCode"></a>

signup-secret.page.html

The QR code is useful when you work on a desktop browser, and you use an authenticator app on the smartphone. However, what if you create a web application designed for mobile devices and most users are going to use a browser on their smartphone or tablet. Unless they have a second mobile device, they can't scan the QR code with the camera.

To solve this, you can insert an a tag with a href attribute and set the value to the same otpauth:// link that is encoded in the QR code. In this example I wrap the QR code image with an a tag.

    <a [href]="qrSafeLink"><img [src]="qrCode"></a>

signup-secret.page.html

When a user tabs on this link on a mobile device, the authenticator app should automatically open and import the new secret. I tested this with the Google Authenticator on Android, and with this configuration, it works fine.


To finish the sign up workflow, the user has to enter the TOTP with the newly added secret. The server also calculates the TOTP, compares the two values and if they match enables the user.

  @PostMapping("/signup-confirm-secret")
  public boolean signupConfirmSecret(@RequestParam("username") String username,
      @RequestParam("code") @NotEmpty String code) {

    var record = this.dsl.select(APP_USER.ID, APP_USER.SECRET).from(APP_USER)
        .where(APP_USER.USERNAME.eq(username)).fetchOne();
    if (record != null) {
      String secret = record.get(APP_USER.SECRET);
      Totp totp = new Totp(secret);
      if (totp.verify(code)) {
        this.dsl.update(APP_USER).set(APP_USER.ENABLED, true)
            .where(APP_USER.ID.eq(record.get(APP_USER.ID))).execute();
        return true;
      }
    }

    return false;
  }

AuthController.java

The user can now login with username, password and TOTP.


This concludes this tutorial about adding a TOTP second-factor authentication to a Spring Security form login. Also, in the second part, we have seen how a signup workflow could be implemented.

The complete source code for the client and the server is hosted on GitHub:
https://github.com/ralscha/blog2019/tree/master/totp