Selenium WebDriver – Multi-Factor Authentication – SMS

Overview:

Multi-factor authentication is a security system which requires more than one authentication method to verify the identity. If your ‘application under test’ has that feature, It might be little bit challenging for you to automate the complete end-to-end flow. TestAutomationGuru has already covered EMail Validation in case if your application includes EMail auth method.

We are assuming that our application somehow sends an authentication code to a given phone number. Our aim here is to get the code programmatically & enter in the application, so that our automated tests can proceed with business workflow. Our aim here is not to send SMS or responding to a SMS even though they are possible!

In this article, Lets see how to automate SMS auth flow.

Twillio:

Twillio is a communication platform which allows us to programmatically make/receive phone calls, SMS & MMS etc. Do note that Twillio is not FREE. However, it is dirt cheap!

  • Register yourself in Twillio
  • Twillio provides an ACCOUNT ID and AUTH TOKEN for using their API. Make a note of them.
  • Buy a phone number from them with SMS capability ($1 per month).

Screenshot from 2018-04-29 09-42-21

  • You can also use their REST API to buy a number programmatically. They also provide a sample code in Java, C#, NodeJs, Ruby, CURL, PHP, Python etc. Our aim here is not to buy tons of numbers. So this is just FYI.

Screenshot from 2018-04-29 09-55-32

  • Once you buy a number – it is ready to receive SMS.
  • Use the number in your application as a test phone number for Multi factor authentication flow.

Lets create a simple utility class which fetches the SMS message.

Maven Dependency:

<dependency>
    <groupId>com.twilio.sdk</groupId>
    <artifactId>twilio</artifactId>
    <version>7.20.0</version>
</dependency>

SMSReader:

public class SMSReader {

    private static final String ACCOUNT_SID = "AC236ekln3498r439r3489fj4e0ddj916aad";
    private static final String AUTH_TOKEN = "d2a145jkfdhry3dhdh993iotg5b14f1";
    private static final String TO_PHONE_NUMBER = "+11234567890"; //test number where we receive SMS

    public SMSReader(){
        Twilio.init(ACCOUNT_SID, AUTH_TOKEN);
    }

    public String getMessage(){
        return getMessages()
                    .filter(m -> m.getDirection().compareTo(Message.Direction.INBOUND) == 0)
                    .filter(m -> m.getTo().equals(TO_PHONE_NUMBER))
                    .map(Message::getBody)
                    .findFirst()
                    .orElseThrow(IllegalStateException::new);
    }
    //deletes all the messages
    public void deleteMessages(){
        getMessages()
                .filter(m -> m.getDirection().compareTo(Message.Direction.INBOUND) == 0)
                .filter(m -> m.getTo().equals(TO_PHONE_NUMBER))
                .map(Message::getSid)
                .map(sid -> Message.deleter(ACCOUNT_SID, sid))
                .forEach(MessageDeleter::delete);

    }
    
    private Stream<Message> getMessages(){
        ResourceSet<Message> messages = Message.reader(ACCOUNT_SID).read();
        return StreamSupport.stream(messages.spliterator(), false);
    }

}

Lets assume that our application has a login screen. Based on some business rules, upon user login, it sends out an auth code to the phone number registered. Now we use our SMSReader utility to read the auth code.

Lets create a simple test.

public class SMSTest {

    private WebDriver driver;
    private LoginPage page;
    private SMSReader sms;

    @BeforeTest
    public void setup(){
        driver = DriverManager.getDriver();
        page = new LoginPage(driver);
        sms = new SMSReader();
    }

    @Test
    public void smsTest(){
        //clean all the existing messages if any
        sms.deleteMessages();

        page.login("username", "password");
        //if the page is asking for SMS code
        if(page.isMFArequired()){
            String code = sms.getMessage();
            page.enterCode(code);
        }
    }

}

Free Online SMS Receiver:

In case you do not like to use Twillio, you could take a look at few online SMS receivers like this site. Basically this site has few numbers from which you could pick one and use it as a test phone numbers for your application.

Screenshot from 2018-04-29 19-11-10

Do note some one else could also use that number at the same time & any one can see the message your application sends. Assuming SMS has just code as shown below and no other sensitive information, You could safely use! Others can not just figure it out what these codes are for! For ex: We do not know which test application in the world sent this message!

Screenshot from 2018-04-29 19-18-00

Unfortunately this site does not have any APIs. Lets come up with our own APIs using JSoup.

Maven Dependency:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version> 1.11.3</version>
</dependency>

SMSUtil:

public class SMSUtil {

    private static final String SMS_RECEIVE_FREE_URL = "https://smsreceivefree.com/country/usa";
    private static final String SMS_MESSAGE_LIST_URL = "https://smsreceivefree.com/info/";
    private static final String NUMBER_CSS_CLASS = "numbutton";
    private static final String TABLE_CSS_SELECTOR = "table.msgTable > tbody > tr";
    private static final String HREF = "href";
    private static final String HTML_TD = "td";
    private static final String FORWARD_SLASH = "/";
    private static final Pattern PATTERN = Pattern.compile("/info/(\\d+)/");

    public String getNumber() throws IOException {
        return Jsoup.connect(SMS_RECEIVE_FREE_URL)           //access the site
                    .get()
                    .body()                                  //get html body
                    .getElementsByClass(NUMBER_CSS_CLASS)    //get all number elements
                    .stream()
                    .map(e -> e.attr(HREF))                  //get href attr value
                    .map(PATTERN::matcher)                   //regex to find the number in the url
                    .filter(Matcher::find)                   //if there is number
                    .findAny()                               //pick any
                    .map(m -> m.group(1))                    //get the number
                    .orElseThrow(IllegalStateException::new);
    }

    public String getMessage(final String phoneNumber, final String fromPhoneNumber) throws IOException {
        return Jsoup.connect(SMS_MESSAGE_LIST_URL.concat(phoneNumber).concat(FORWARD_SLASH))   //access the site
                    .get()
                    .body()                                                                        //get html body
                    .select(TABLE_CSS_SELECTOR)
                    .stream()
                    .map(tr -> tr.getElementsByTag(HTML_TD))                                       //get all cells
                    .filter(tds -> tds.get(0)
                                            .text()
                                            .trim()
                                            .startsWith(fromPhoneNumber))                          //if the number starts with given number
                    .findFirst()                                                                   //find first match
                    .map(tds -> tds.get(2))                                                        //pick 3rd cell
                    .map(td -> td.text().trim())                                                   //get cell value
                    .orElseThrow(IllegalStateException::new);
    }

}

SMS Test:

public class SMSTest {

    private WebDriver driver;
    private LoginPage page;
    private SMSUtil sms;
    private final String fromPhoneNumber = "1234567"; //this is the number your application uses

    @BeforeTest
    public void setup(){
        driver = DriverManager.getDriver();
        page = new LoginPage(driver);
        sms = new SMSUtil();
    }

    @Test
    public void smsTest(){
        //get a phone number
        String phoneNumber = sms.getNumber();

        //use the number in the application
        page.setNumber(phoneNumber);
        
        //some code which triggers application to send a SMS
        ...
        ...
        //get SMS
        String code = sms.getMessage(phoneNumber, fromPhoneNumber);

        //use the code
        ...
    }

}

Summary:

Automating Multi factor authentication with SMS is not a big challenge by using Twillio / JSoup with online SMS receivers.  Both approaches worked just fine for me. Twillio provides good privacy & flexibility at a reasonable price! So, I would prefer Twillio.

Happy Testing & Subscribe 🙂

 

 

Share This:

9 thoughts on “Selenium WebDriver – Multi-Factor Authentication – SMS

    1. Possible. But not a good idea. Our aim is to extract the information.. So, using API is the way to go!

  1. very nice, can you also write an article on handling 2FA in Loadrunner or Jmeter. or if you have done such work please share..

    thanks

  2. Hi Sir, thanks for the information. Is it possible to retrieve the message from a short code/ long code which application may procured with different vendors.

  3. ResourceSet messages = Message.reader(ACCOUNT_SID).read();
    return StreamSupport.stream(messages.spliterator(), false);
    We have illegal execption on this code .Kindly help me e

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.