SMS Two-Factor Authentication with Java and Servlets

About this Application

This Java Servlets application example demonstrates how to implement a SMS two-factor authentication using Twilio.

To run this application yourself download the code and follow the instructions on GitHub.

Adding a two-factor authentication (2FA) to your web application increases the security of your user's data. Multi-factor authentication determines the identity of a user in two steps:

  1. First, we validate the user with an email and password

  2. Second, we validate by sending them a one-time verification code to a mobile device

Once our user enters the verification code, we know they have received the SMS, and indeed they are who they say they are. This is a standard SMS implementation.

For a slightly more advanced implementation using Authy One-Touch take a look at this tutorial.

Intuit uses Twilio SMS to protect 1M+ businesses from online security threats. Read why they chose Twilio.

Generate a Verification Code

Once our user logs in we need to send them the one-time verification code.

To generate our verification code we use the java.util.Random class. Considering the current implementation, our 6-digit verification code could be any number between 100000 and 999999.

Loading Code Samples...
Language
package com.twilio.sms2fa.domain.model;

import com.twilio.sms2fa.domain.exception.WrongVerificationCodeException;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;
import org.mindrot.jbcrypt.BCrypt;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.Random;
import java.util.UUID;

@Entity
public class User {

    private static final int MAX_VERIFICATION_CODE = 100000;
    private static final int MIN_VERIFICATION_CODE = 999999;

    @Id
    private String id;

    @Column(name = "FIRST_NAME")
    @NotBlank(message = "First Name may not be blank")
    private String firstName;

    @Column(name = "LAST_NAME")
    @NotBlank(message = "Last Name may not be blank")
    private String lastName;

    @Column(name = "EMAIL", unique = true)
    @NotBlank(message = "Email may not be blank")
    @Email(message = "Email format does not match")
    private String email;

    @NotBlank(message = "Password may not be blank")
    @Column(name = "PASSWORD")
    private String password;

    @NotBlank(message = "Phone Number may not be blank")
    @Column(name = "PHONE_NUMBER")
    private String phoneNumber;

    @Column(name = "VERIFICATION_CODE")
    private String verificationCode;

    @Column(name = "CONFIRMED")
    private boolean confirmed;

    // required by orm
    public User() {
    }

    public User(final String firstName, final String lastName,
                final String email, final String phoneNumber,
                final String password) {
        this.id = UUID.randomUUID().toString();
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.confirmed = false;
        this.password = BCrypt.hashpw(password, BCrypt.gensalt());
        this.verificationCode = generateVerificationCode();
    }

    static String generateVerificationCode() {
        Random rand = new Random();
        Integer code = rand.nextInt(MIN_VERIFICATION_CODE
                - MAX_VERIFICATION_CODE + 1) + MAX_VERIFICATION_CODE;
        return code.toString();
    }

    public void confirm(final String verificationCode) {
        if (!this.verificationCode.equals(verificationCode)) {
            throw new WrongVerificationCodeException(verificationCode);
        }
        confirmed = true;
    }

    public void generateNewVerificationCode() {
        this.verificationCode = generateVerificationCode();
    }

    public boolean authenticate(final String password) {
        return BCrypt.checkpw(password, this.password);
    }

    public String getId() {
        return id;
    }

    public String getVerificationCode() {
        return verificationCode;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public String getEmail() {
        return email;
    }

    public boolean isConfirmed() {
        return confirmed;
    }

}
src/main/java/com/twilio/sms2fa/domain/model/User.java
Generate a Verification Code

src/main/java/com/twilio/sms2fa/domain/model/User.java

Next, let's take a look at how to setup the Twilio Java helper library to send the verification code as an SMS.

Obtain a TwilioRestClient Instance

The Twilio helper library allows us to easily send an SMS. First, we have to create an instance of a Twilio Client with our credentials.

Loading Code Samples...
Language
package com.twilio.sms2fa.infrastructure.guice;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.sms2fa.domain.service.ConfirmUser;
import com.twilio.sms2fa.domain.service.CreateUser;
import com.twilio.sms2fa.domain.service.MessageSender;
import com.twilio.sms2fa.infrastructure.service.TwilioMessageSender;
import ru.vyarus.guice.validator.ImplicitValidationModule;

import static com.twilio.sms2fa.infrastructure.guice.ApplicationProperties
        .TWILIO_ACCOUNT_SID;
import static com.twilio.sms2fa.infrastructure.guice.ApplicationProperties
        .TWILIO_AUTH_TOKEN;

class Sms2faServiceModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(MessageSender.class).to(TwilioMessageSender.class);
        bind(CreateUser.class);
        bind(ConfirmUser.class);

        install(new ImplicitValidationModule());
    }

    @Provides
    public TwilioRestClient twilioRestClient(
            @Named(TWILIO_ACCOUNT_SID) final String twilioAccountSid,
            @Named(TWILIO_AUTH_TOKEN) final String twilioAuthToken) {
        return new TwilioRestClient
                .Builder(twilioAccountSid, twilioAuthToken)
                .build();
    }

}
src/main/java/com/twilio/sms2fa/infrastructure/guice/Sms2faServiceModule.java
Obtain a TwilioRestClient Instance

src/main/java/com/twilio/sms2fa/infrastructure/guice/Sms2faServiceModule.java

You can find your credentials on your Twilio Account.

Next, we will see how to actually send the verification code.

Send a Verification Code

Once we have the MessageCreator instance created with the necessary parameters, all we have to do to send an SMS using the REST API is to call the execute method passing the twilioRestClient instance.

In this example, the class TwilioMessageSender is the one responsible for it.

Loading Code Samples...
Language
package com.twilio.sms2fa.infrastructure.service;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.rest.api.v2010.account.MessageCreator;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.service.MessageSender;
import com.twilio.type.PhoneNumber;

@Singleton
public class TwilioMessageSender implements MessageSender {

    private static final String QUEUED = "queued";

    private TwilioRestClient twilioRestClient;
    private PhoneNumber fromPhoneNumber;

    @Inject
    public TwilioMessageSender(
            final TwilioRestClient twilioRestClient,
            @Named("TWILIO_PHONE_NUMBER") final String fromPhoneNumber) {
        this.twilioRestClient = twilioRestClient;
        this.fromPhoneNumber = new PhoneNumber(fromPhoneNumber);
    }

    @Override
    public final boolean sendCode(final User user) {
        final PhoneNumber to = new PhoneNumber(user.getPhoneNumber());
        final PhoneNumber from = fromPhoneNumber;
        final String body = user.getVerificationCode();

        Message message = new MessageCreator(to, from, body)
                .create(twilioRestClient);

        return QUEUED.equals(message.getStatus());
    }
}
src/main/java/com/twilio/sms2fa/infrastructure/service/TwilioMessageSender.java
Send a Verification Code

src/main/java/com/twilio/sms2fa/infrastructure/service/TwilioMessageSender.java

Now that we know how to generate the verification code and send it, let's now look at how to kick off the signup process.

Register a User

When a user signs up on our website, the prior should store the user's information and send them a verification code.

In order to do two-factor authentication we need to make sure we ask for the user's phone number.

One thing to notice is that when the User constructor is called, the verification code is generated.

Loading Code Samples...
Language
package com.twilio.sms2fa.application.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.sms2fa.application.constants.ExternalResource;
import com.twilio.sms2fa.application.constants.InternalResource;
import com.twilio.sms2fa.application.util.ServletUtil;
import com.twilio.sms2fa.domain.exception.DomainException;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.service.CreateUser;

import javax.persistence.PersistenceException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;

@Singleton
public class UsersServlet extends HttpServlet {

    private CreateUser createUser;

    @Inject
    public UsersServlet(final CreateUser createUser) {
        this.createUser = createUser;
    }

    @Override
    protected void doPost(final HttpServletRequest request,
                          final HttpServletResponse response) throws
            ServletException, IOException {
        String firstName = request.getParameter("first_name");
        String lastName = request.getParameter("last_name");
        String email = request.getParameter("email");
        String phoneNumber = request.getParameter("phone_number");
        String password = request.getParameter("password");
        try {
            User user = createUser.create(new User(firstName, lastName, email,
                    phoneNumber, password));
            request.getSession().setAttribute("user", user);
            response.sendRedirect(ExternalResource.CONFIRMATIONS_NEW.getPath());
        } catch (ConstraintViolationException | DomainException
                | PersistenceException e) {
            ServletUtil.handleException(e, request, response,
                    InternalResource.USERS_NEW_JSP.getPath());
        }

    }
}
src/main/java/com/twilio/sms2fa/application/servlets/UsersServlet.java
Register a User

src/main/java/com/twilio/sms2fa/application/servlets/UsersServlet.java

Let's see how the CreateUser#create method is implemented.

Save and Send the SMS

In this step all we will do is store the user (and his verification code accordingly) and send an SMS message.

Noticed that MessageSender is implemented by TwilioMessageSender, that we mentioned previously.

Loading Code Samples...
Language
package com.twilio.sms2fa.domain.service;

import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.repository.UserRepository;

import javax.validation.Valid;

public class CreateUser {

    private UserRepository userRepository;
    private MessageSender messageSender;

    @Inject
    public CreateUser(
            final UserRepository userRepository,
            final MessageSender messageSender) {
        this.userRepository = userRepository;
        this.messageSender = messageSender;
    }

    @Transactional
    public User create(@Valid final User user) {
        User savedUser = userRepository.save(user);
        messageSender.sendCode(savedUser);
        return savedUser;
    }
}
src/main/java/com/twilio/sms2fa/domain/service/CreateUser.java
Save and Send the SMS

src/main/java/com/twilio/sms2fa/domain/service/CreateUser.java

Now let's take a closer at how to proceed with the 2-step verification.

Implement the 2-Step Verification

When the user receives an SMS with the verification code we need to ensure the given code is valid.

This validation is achieved by comparing the user's verification code with the verification code the user inputs on the form.

Confirm Verification Code

If the validation was successful the application allows the user to have access to the protected content. Otherwise the application will prompt for the verification code once again.

Loading Code Samples...
Language
package com.twilio.sms2fa.application.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.sms2fa.application.constants.ExternalResource;
import com.twilio.sms2fa.application.constants.InternalResource;
import com.twilio.sms2fa.domain.exception.DomainException;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.service.ConfirmUser;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Singleton
public class ConfirmationsServlet extends HttpServlet {

    private ConfirmUser confirmUser;

    @Inject
    public ConfirmationsServlet(final ConfirmUser confirmUser) {
        this.confirmUser = confirmUser;
    }

    @Override
    protected void doPost(final HttpServletRequest request,
                          final HttpServletResponse response)
            throws ServletException, IOException {
        try {
            String verificationCode = request.getParameter("verification_code");
            User user = (User) request.getSession().getAttribute("user");
            confirmUser.confirm(user, verificationCode);
            request.getSession().setAttribute("authenticated", true);
            response.sendRedirect(ExternalResource.SECRETS.getPath());
        } catch (DomainException e) {
            request.setAttribute("errorMessage", e.getMessage());
            request.getRequestDispatcher(InternalResource
                    .CONFIRMATIONS_NEW_JSP.getPath())
                    .forward(request, response);
        }
    }

}
src/main/java/com/twilio/sms2fa/application/servlets/ConfirmationsServlet.java
Implement the 2-Step Verification

src/main/java/com/twilio/sms2fa/application/servlets/ConfirmationsServlet.java

That's it! We've just implemented SMS Two-Factor Authentication that you can now use in your applications!

Where to next?

If you're a Java developer working with Twilio, you might want to check these other tutorials out.

Workflow Automation

Increase your rate of response by automating the workflows that are key to your business. In this tutorial, you will learn how to build a ready-for-scale automated SMS workflow, for a vacation rental company.

Masked Numbers

Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS. Learn how to create disposable phone numbers on-demand, so two users can communicate without exchanging personal information.

Did this help?

Thanks for checking this tutorial out! If you have any feedback to share with us please contact us on Twitter, we'd love to hear it.

Agustin Camino
David Prothero
Andrew Baker

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
package com.twilio.sms2fa.domain.model;

import com.twilio.sms2fa.domain.exception.WrongVerificationCodeException;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;
import org.mindrot.jbcrypt.BCrypt;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.Random;
import java.util.UUID;

@Entity
public class User {

    private static final int MAX_VERIFICATION_CODE = 100000;
    private static final int MIN_VERIFICATION_CODE = 999999;

    @Id
    private String id;

    @Column(name = "FIRST_NAME")
    @NotBlank(message = "First Name may not be blank")
    private String firstName;

    @Column(name = "LAST_NAME")
    @NotBlank(message = "Last Name may not be blank")
    private String lastName;

    @Column(name = "EMAIL", unique = true)
    @NotBlank(message = "Email may not be blank")
    @Email(message = "Email format does not match")
    private String email;

    @NotBlank(message = "Password may not be blank")
    @Column(name = "PASSWORD")
    private String password;

    @NotBlank(message = "Phone Number may not be blank")
    @Column(name = "PHONE_NUMBER")
    private String phoneNumber;

    @Column(name = "VERIFICATION_CODE")
    private String verificationCode;

    @Column(name = "CONFIRMED")
    private boolean confirmed;

    // required by orm
    public User() {
    }

    public User(final String firstName, final String lastName,
                final String email, final String phoneNumber,
                final String password) {
        this.id = UUID.randomUUID().toString();
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.confirmed = false;
        this.password = BCrypt.hashpw(password, BCrypt.gensalt());
        this.verificationCode = generateVerificationCode();
    }

    static String generateVerificationCode() {
        Random rand = new Random();
        Integer code = rand.nextInt(MIN_VERIFICATION_CODE
                - MAX_VERIFICATION_CODE + 1) + MAX_VERIFICATION_CODE;
        return code.toString();
    }

    public void confirm(final String verificationCode) {
        if (!this.verificationCode.equals(verificationCode)) {
            throw new WrongVerificationCodeException(verificationCode);
        }
        confirmed = true;
    }

    public void generateNewVerificationCode() {
        this.verificationCode = generateVerificationCode();
    }

    public boolean authenticate(final String password) {
        return BCrypt.checkpw(password, this.password);
    }

    public String getId() {
        return id;
    }

    public String getVerificationCode() {
        return verificationCode;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public String getEmail() {
        return email;
    }

    public boolean isConfirmed() {
        return confirmed;
    }

}
package com.twilio.sms2fa.infrastructure.guice;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.sms2fa.domain.service.ConfirmUser;
import com.twilio.sms2fa.domain.service.CreateUser;
import com.twilio.sms2fa.domain.service.MessageSender;
import com.twilio.sms2fa.infrastructure.service.TwilioMessageSender;
import ru.vyarus.guice.validator.ImplicitValidationModule;

import static com.twilio.sms2fa.infrastructure.guice.ApplicationProperties
        .TWILIO_ACCOUNT_SID;
import static com.twilio.sms2fa.infrastructure.guice.ApplicationProperties
        .TWILIO_AUTH_TOKEN;

class Sms2faServiceModule extends AbstractModule {

    @Override
    protected void configure() {
        bind(MessageSender.class).to(TwilioMessageSender.class);
        bind(CreateUser.class);
        bind(ConfirmUser.class);

        install(new ImplicitValidationModule());
    }

    @Provides
    public TwilioRestClient twilioRestClient(
            @Named(TWILIO_ACCOUNT_SID) final String twilioAccountSid,
            @Named(TWILIO_AUTH_TOKEN) final String twilioAuthToken) {
        return new TwilioRestClient
                .Builder(twilioAccountSid, twilioAuthToken)
                .build();
    }

}
package com.twilio.sms2fa.infrastructure.service;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.twilio.http.TwilioRestClient;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.rest.api.v2010.account.MessageCreator;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.service.MessageSender;
import com.twilio.type.PhoneNumber;

@Singleton
public class TwilioMessageSender implements MessageSender {

    private static final String QUEUED = "queued";

    private TwilioRestClient twilioRestClient;
    private PhoneNumber fromPhoneNumber;

    @Inject
    public TwilioMessageSender(
            final TwilioRestClient twilioRestClient,
            @Named("TWILIO_PHONE_NUMBER") final String fromPhoneNumber) {
        this.twilioRestClient = twilioRestClient;
        this.fromPhoneNumber = new PhoneNumber(fromPhoneNumber);
    }

    @Override
    public final boolean sendCode(final User user) {
        final PhoneNumber to = new PhoneNumber(user.getPhoneNumber());
        final PhoneNumber from = fromPhoneNumber;
        final String body = user.getVerificationCode();

        Message message = new MessageCreator(to, from, body)
                .create(twilioRestClient);

        return QUEUED.equals(message.getStatus());
    }
}
package com.twilio.sms2fa.application.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.sms2fa.application.constants.ExternalResource;
import com.twilio.sms2fa.application.constants.InternalResource;
import com.twilio.sms2fa.application.util.ServletUtil;
import com.twilio.sms2fa.domain.exception.DomainException;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.service.CreateUser;

import javax.persistence.PersistenceException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;

@Singleton
public class UsersServlet extends HttpServlet {

    private CreateUser createUser;

    @Inject
    public UsersServlet(final CreateUser createUser) {
        this.createUser = createUser;
    }

    @Override
    protected void doPost(final HttpServletRequest request,
                          final HttpServletResponse response) throws
            ServletException, IOException {
        String firstName = request.getParameter("first_name");
        String lastName = request.getParameter("last_name");
        String email = request.getParameter("email");
        String phoneNumber = request.getParameter("phone_number");
        String password = request.getParameter("password");
        try {
            User user = createUser.create(new User(firstName, lastName, email,
                    phoneNumber, password));
            request.getSession().setAttribute("user", user);
            response.sendRedirect(ExternalResource.CONFIRMATIONS_NEW.getPath());
        } catch (ConstraintViolationException | DomainException
                | PersistenceException e) {
            ServletUtil.handleException(e, request, response,
                    InternalResource.USERS_NEW_JSP.getPath());
        }

    }
}
package com.twilio.sms2fa.domain.service;

import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.repository.UserRepository;

import javax.validation.Valid;

public class CreateUser {

    private UserRepository userRepository;
    private MessageSender messageSender;

    @Inject
    public CreateUser(
            final UserRepository userRepository,
            final MessageSender messageSender) {
        this.userRepository = userRepository;
        this.messageSender = messageSender;
    }

    @Transactional
    public User create(@Valid final User user) {
        User savedUser = userRepository.save(user);
        messageSender.sendCode(savedUser);
        return savedUser;
    }
}
package com.twilio.sms2fa.application.servlets;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.twilio.sms2fa.application.constants.ExternalResource;
import com.twilio.sms2fa.application.constants.InternalResource;
import com.twilio.sms2fa.domain.exception.DomainException;
import com.twilio.sms2fa.domain.model.User;
import com.twilio.sms2fa.domain.service.ConfirmUser;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Singleton
public class ConfirmationsServlet extends HttpServlet {

    private ConfirmUser confirmUser;

    @Inject
    public ConfirmationsServlet(final ConfirmUser confirmUser) {
        this.confirmUser = confirmUser;
    }

    @Override
    protected void doPost(final HttpServletRequest request,
                          final HttpServletResponse response)
            throws ServletException, IOException {
        try {
            String verificationCode = request.getParameter("verification_code");
            User user = (User) request.getSession().getAttribute("user");
            confirmUser.confirm(user, verificationCode);
            request.getSession().setAttribute("authenticated", true);
            response.sendRedirect(ExternalResource.SECRETS.getPath());
        } catch (DomainException e) {
            request.setAttribute("errorMessage", e.getMessage());
            request.getRequestDispatcher(InternalResource
                    .CONFIRMATIONS_NEW_JSP.getPath())
                    .forward(request, response);
        }
    }

}