How I reinvented a wheel: Building SMS auth in Vertx with Java

A SMS authentication is a form of a password less authentication. Users log in with its username and then receives a confirmation code on a mobile phone that he/she should enter in order to receive an access. There are different opinions against this method of authentication, however you could not argue against the fact that for mobile apps it is an easiest and most logical for users way to login. Many mobile apps, like taxi, delivery etc. use such method a lot.

Do you want to learn more about Vert.x framework and Java microservices? Take a look on the complete list of my Vertx tutorials and case studies.

The need

Due to the fact, that I develop backends mostly for mobile apps, I decided to implement my own solution. Anyway, it is out of scope of this post to talk about to use or not SMS authentication.

There are ways to do a SMS authentication. The most simplest way is to rely on an authentication as a service provider, such as Auth0 or Okta, and integrate their solutions. However, when your users’ data is stored on a third-party side, it may not be protected (or as my Digital Forensics professor says , you need to choose between convenience and security), also it brings complications in cases, when an user management is performed on your side.

So there, I developed (or reinvented) a simple solution using Vertx. I use a third-party SMS sender API to send SMS messages. Of course, a concrete implementation depends on your provider.

Building a custom Vertx auth

Vertx provides a very neat way to implement authentication in your apps. In general, you need to perform following steps:

  1. Create an AuthProvider
  2. Create an AuthHandler with AuthProvider
  3. Assign an AuthHandler to protected routes

When user tries to open the protected route, AuthHandler parses Authorization header, extracts required credentials (username-password, token etc.) and passes it to AuthProvider. AuthProvider verifies credentials and grants or denies access. When you use JWT authorization, Vertx provides JWTAuth class that is an AuthProvider for a JWT authorization. However, it is not the best one and there are several pitfalls. I recommend you to implement your own AuthProvider. It is easy: just implement JWTAuth. In a very basic condition, your AuthProvider should look like this:

public class AuthProvider implements JWTAuth{

	//Constructor
	public AuthProvider(){

	}

	@Override
	public void authentificate(JsonObject data, Handler<AsyncResult<User>> handler){

	}

	@Override
    public String generateToken(JsonObject data, JWTOptions jwtOptions) {
    	return "Token";
    }
}

There two methods we implement from JWTAuth: authentificate and generateToken. The second method generates a JWT token with a list of defined JWT claims. The first method is much more interesting. Its first argument is a JSON object containing information for authenticating the user. As it was noticed it depends on implementation. In a case of JWT, we need to get jwt value. The second argument is a User object that is passed to the handler in an AsyncResult. This user can then be used for authorisation. When you do provide your own AuthProvider implementation it is advised to have an own User implementation. You should inherit from AbstractUser class. Check this code snippet, implementing our AuthUser:

public class AuthUser extends AbstractUser {

    private String name;
    private String role;

    public AuthUser(String name, String role){
        this.name = name;
        this.role = role;
    }

    @Override
    protected void doIsPermitted(String permission, Handler<AsyncResult<Boolean>> handler) {
        //if a requested role equals to user's role, we give a permission
        handler.handle(Future.succeededFuture(permission.equalsIgnoreCase(role)));
    }

    /* Other overriden methods*/
}

When you have a AuthUser class defined, you can implement your AuthProvider. In my app I use a Nimbus Jose+JWT library to build a token verification/generation. First, we do a token generation:

    @Override
    public String generateToken(JsonObject data, JWTOptions jwtOptions) {

        try{

            JWSSigner signer = new ECDSASigner(secretKey);

            //get payload
            String subject = data.getString("sub");
            String role = data.getString("role");

            //set expiration time -> token is valid for 30 days
            LocalDateTime expired = LocalDateTime.now().plusDays(30);

            /*convert "expired" to Date, because JWTClaimsSet accepts Date as an argument*/
            Date date = Date.from(expired.atZone(ZoneId.systemDefault()).toInstant());

            //create JWT claims
            JWTClaimsSet claims = new JWTClaimsSet.Builder().subject(subject).expirationTime(date).claim("role", role).build();

            //create token and sign
            SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.ES256)
                    .keyID(secretKey.getKeyID()).build(),
                    claims);
            signedJWT.sign(signer);

            //return create token
            return signedJWT.serialize();

        } catch (Exception ex){

            //Log error to STDOUT
            System.out.println("Unable to generate token: "+ex.getLocalizedMessage());

            //return null result
            return null;
        }

    }

The concrete approach depends on key library you are using, in my case, I use EC keys and JoseJWT library. It is out of scope of this post, so I guide you to JoseJWT documentation. In other things, code is a pretty self-explanative. Next step is to authentificate a user:

    @Override
    public void authenticate(JsonObject data, Handler<AsyncResult<User>> handler) {

        //get JWT token
        String token = data.getString("jwt");

        //check if token is valid or not

        try{

            //verify token

            JWSVerifier verifier = new ECDSAVerifier(publicKey);
            SignedJWT signedJWT = SignedJWT.parse(token);

            if (!signedJWT.verify(verifier)){
                //we cannot verify this token -> throw Exception
                throw new Exception();
            }

            //get appUser role from token
            String role = signedJWT.getJWTClaimsSet().getStringClaim("role");

            //create AppUser object with specified permission
            AppUser appUser = new AppUser(role);

            //User was authentificated successfully
            handler.handle(Future.succeededFuture(appUser));

        } catch (Exception ex){

            //Log error to STDOUT
            System.out.println(ex.getLocalizedMessage());

            //user is not authorized
            handler.handle(Future.failedFuture(ex));

        }

    }

I used a parameter role to define user’s role (user, admin etc.). You can use Authorities to specify user’s roles and permissions. In my code it is simpler to use just a String variable to define a role, but if you have several permissions, you should consider authorities.

Do you want to learn more about Vert.x framework and Java microservices? Take a look on the complete list of my Vertx tutorials and case studies.

A login flow

A login flow is implemented inside AuthVerticle. Generally, we should perform these steps:

  1. User logins with route /login/:username
  2. App finds a user with specified username in a data source
  3. If user does not exist we return a 404 code
  4. If user exists, we need to extract a user’s phone and send on it a generated 4-digit code.
  5. Send a SMS with code to user. Store a code + an username and date/time of issue in a temporal storage (for instance, Redis or in a database)
  6. User goes to route code/:username/:code
  7. Check if a combination of username + code exists
  8. Assert that date is not expieried
  9. Delete a code from database (to prevent a repeated usage of code)
  10. Issue user’s token based on user’s permissions

Check this code snippet (extraction):

router.get("/login/:username").handler(c->{
            String username = c.pathParam("username");
            Credentials creds = credentialsDao.findCredentialsByUsername(username); //step 2
            if (creds==null){
                //step 3.
                c.response().setStatusCode(404).end("Not found");
            }
            //step 4
            String phone = creds.getPhone();
            //CodeGenerator is a wrapper to extract dependency
            String code = CodeGenerator.generate();

            //step 5
            //send SMS with code to user
            smsService.sendMessage(phone, code);

            //store
            codeRequestDao.store(username, code, LocalDateTime.now());
            c.response().setStatusCode(201).end();
        });


router.get("/code/:username/:code").handler(c->{
            String code = c.pathParam("code");
            String username = c.pathParam("username");
            //step 7
            CodeRequest codeRequest = codeRequestDao.find(username, code);
            if (codeRequest == null){
                c.response().setStatusCode(403).end("Unauthorized");
            }
            if (codeRequest.isExpired()){
                //step 8
                c.response().setStatusCode(403).end("Unauthorized");
            }
            //Step 9
            codeRequestDao.delete(codeRequest.getId());
            Credentials creds = credentialsDao.findCredentialsByUsername(username);
            //setting JWT claims
            JsonObject claims = new JsonObject();
            claims.put("role", creds.getRole());
            claims.put("sub", creds.getUsername());
            //...other claims can be specified
            //Step 10
            String token = authProvider.generateToken(claims, new JWTOptions());
            c.response().setStatusCode(201).putHeader("Authorization", token).end("Access granted");
});

Then, we just secure protected routes with AuthHandler:

router.route("/private/*").handler(JWTAuthHandler.create(provider));

Try it yourself

There is an example application, that illustrates this approach. Of course, it is not a ready-to-production service, however it does its function. You can clone it from this github repo:

git clone https://github.com/mednikovnet/vertx-sms-auth

Then you need to do some setup in order to run it. Check readme.md for more details.

Conclusion

That all folks! As you can see, it is very easy to implement a SMS authentication with Vertx. Of course there are other things, like user management, sms provider, SMS code generation that are out side of scope of this example.

Do you want to learn more about Vert.x framework and Java microservices? Take a look on the complete list of my Vertx tutorials and case studies.

References

  • Rachit Gulati. JWT to authenticate Servers API’s, 2018 Read
  • Piotr Mińkowski. Building Secure APIs with Vert.x and OAuth2, 2017 Read
  • Authentication and authorization using Auth0 and Vertx, 2016 Read