Skip to content

Two-factor authentication with Google Authenticator (part 2/2)

Hello again! This is the second and final part of my Google Authenticator with Vertx tutorial. As it was promised in the previous part, this time we would finish our example and implement a login flow. I advise you follow the previous part or grab a code from my github. Let start

Implementing a login flow

In this post we would finish our example application and implement a login flow for 2-factor authentication with Vert.x and GoogleAuthenticator library.

Step 1. Create a login handler

We’ve already touched a login handler and have created an empty doLogin() function in our verticle. Let add code now:

private void doLogin(RoutingContext ctx){
        // 1. Get data from view
        JsonObject req = ctx.getBodyAsJson();
        String username = req.getString("username");
        // 2. Find a saved user
        Optional<User> query = usersRepository.findByUsername(username);
        if (query.isPresent()){
            User user = query.get();
            // 3. assert that passwords match
            String password = req.getString("password");
            boolean doPasswordsMatch = password.equalsIgnoreCase(user.getPassword());
            if (doPasswordsMatch){
                // 4. Retrieve saved key
                String key = user.getKey();
                int code = Integer.parseInt(req.getString("code"));
                // 5. Check that TOTP code is correct
                boolean isAuthorized = authenticator.authorize(key, code);
                if (isAuthorized){
                    ctx.response().setStatusCode(200).end();
                } else {
                    ctx.response().setStatusCode(403).end();
                }
            } else {
                ctx.response().setStatusCode(403).end();
            }
        } else {
            ctx.response().setStatusCode(404).end();
        }
}

This code snippet is very self-explanatory, but it is my duty to describe steps anyway:)

  1. We get payload from POST request from client
  2. Find saved user in data storage (in our case in in-memory data structure)
  3. Match passwords NB! Of course, in real life apps things are more complicated, because security is very vulnerable topic. You should use salt and hash or even better – secure remote passwords
  4. Retrieving saved secret key
  5. Check that TOTP code, generated by Google Authenticator is correct

Of course, in order to test login, we first need to sign up a user. Let continue and create a view

Step 2. Show a view

In our application we use Apache Freemarker templates. So we need to design a template (step 1) and to write a view handler (step 2). Check code below:

Template:

<!DOCTYPE html>
<html>
    <head>
        <title>Title</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <script
        			  src="https://code.jquery.com/jquery-3.4.1.min.js"
        			  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
        			  crossorigin="anonymous"></script>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.css" integrity="sha256-ujE/ZUB6CMZmyJSgQjXGCF4sRRneOimQplBVLu8OU5w=" crossorigin="anonymous" />
    </head>
    <body>
        <div>
            <div class="columns">
                <div class="column">
                    <div class="field">
                        <label class="label">Email</label>
                        <input class="input" type="text" id="login-username-field">
                    </div>
                    <div class="field">
                        <label class="label">Password</label>
                        <input class="input" type="password" id="login-password-field">
                    </div>
                    <button class="button is-primary" onclick="login()">Log in</button>
                </div>
                <div class="column">
                    <div class="field" id="login-code-form">
                        <label class="label">Please insert one-time code</label>
                        <input class="input" type="text" id="login-code-field">
                    </div>
                    <div class="notification is-success" id="login-success-notification">
                        <p>Code is correct, access granted</p>
                    </div>
                    <div class="notification is-danger" id="login-error-notification">
                        <p>Code is incorrect!</p>
                    </div>
                </div>
            </div>
        </div>
    </body>
    <script>
        <!-- Code -->
    </script>
</html>

Save this template as resources/templates/login.ftl. The second step is to write a view handler inside router:

private void showLogin(RoutingContext ctx){
    templateEngine.render(ctx.data(), "templates/login.ftl", res->{
        if (res.succeeded()){
            ctx.response().end(res.result());
        }
    });
}

Now, you could run app and navigate to localhost:4567/login. Something similar should be:

Finally, we have to connect api with view by writing some JQuery code. Take a look on next step.

Step 3. Connect a view and a handler

In this step we connect a view with api handler together. In other words we would write some JS (better say JQuery) code. As in the first part we don’t use static files and put everything into templates. So, code would live between that <script> tags. Take a look on code snippet below:

function login(){
    // step 1. Get data
    var code = $("#login-code-field").val();
    var username = $("#login-username-field").val();
    var password = $("#login-password-field").val();
    // Step 2. Create a request's body
    var payload = {"username": username, "password": password, "code":code};
    // Step 3. Do ajax call
    $.ajax({
        url: 'http://localhost:4567/api/login',
        data: JSON.stringify(payload),
        method: 'POST',
        // Step 4. Check results
        statusCode: {
            200: function(xhr){
                $("#login-success-notification").show();
                $("#login-error-notification").hide();
            },
            403: function(xhr){
                $("#login-success-notification").hide();
                $("#login-error-notification").show();
            },
            404: function(xhr){
                $("#login-success-notification").hide();
                $("#login-error-notification").show();
            }
        }
    });
}

First 3 steps basically don’t differ from the fist part’s frontend code. The key difference is that in this part we use statusCode. Why and what is it? The answer on the question why is simple: you remember, that we use 3 different HTTP response codes in order to differentiate a server response: 200 (ok), 403 (not authorized) and 404 (when user is not found). The second part of question is that using the statusCode makes easier to handle such cases. Think about it as switch (although, technically they are not purely same).

Also, I would like to add onLoad handler to hide messages until we need them. JQuery does it with document.ready method.

$(document).ready(function(){
    $("#login-success-notification").hide();
    $("#login-error-notification").hide();
});

Step 4. Testing all together

After assembling everything, we can test how our app works. NB!: first step to signup a user, don’t forget to create a user before log in. You now could navigate to localhost:4567/login and enter your credentials and code, generated by Google Authenticator app:

If you enter wrong code or credentials, you would get an error message:

That is all – we have implement a signup/login process with TOTP codes for Vert.x applications. You can use such approach not only for MVC apps, but for REST APIs as well.

Conclusion

In the 2-part series we talked about one-time passwords and then implemented signup and login flows – both on backend and frontend levels. We also configured Google Authenticator app to use on smartphone. This approach can be used both in MVC apps (as in example), but with REST API. Meanwhile, I encourage you to follow my twitter andreevi_ch to be informed about my new posts. If you have questions, write me message or drop comment below. Have a nice day!