SMS User Authentication With Vapor and AWS

In this SMS user authentication tutorial, you’ll learn how to use Vapor and AWS SNS to authenticate your users with their phone numbers. By Natan Rolnik.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Returning the User and the Session Token

Right below the code you added in UserController.swift, add this:

private func verificationResponseForValidUser(
  with phoneNumber: String,
  on req: Request) -> EventLoopFuture< UserVerificationResponse> {
  // 1
  return User.query(on: req.db)
    .filter(\.$phoneNumber == phoneNumber)
    .first()
    // 2
    .flatMap { queriedUser -> EventLoopFuture<User> in
      if let existingUser = queriedUser {
        return req.eventLoop.future(existingUser)
      }

      return User(phoneNumber: phoneNumber).save(on: req)
    }
    .flatMap { user -> EventLoopFuture<UserVerificationResponse> in
      // 3
      return try! Token.generate(for: user)
        .save(on: req)
        .map {
          UserVerificationResponse(
            status: "ok",
            user: user,
            sessionToken: $0.value)
      }
    }
  }
}

There’s a lot going on here, but it’ll all make sense if you look at everything piece by piece:

  1. First, look for an existing user with the given phone number.
  2. queriedUser is optional because the user might not exist yet. If an existing user is found, it’s immediately returned in EventLoopFuture. If not, create and save a new one.
  3. Finally, create a new Token for this user and save it. Upon completion, map it to the response with the user and the session token.

Build and run your server. It should compile without any issues. Now it’s time to call your APIs!

Testing the APIs With cURL

In the following example, you’ll use curl in the command line, but feel free to use another GUI app you might feel comfortable with, such as Postman or Paw.

Now open Terminal and execute the following command, replacing +1234567890 with your phone number. Don’t forget your country code:

curl -X "POST" "http://localhost:8080/users/send-verification-sms" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{ "phoneNumber": "+1234567890" }'

Oops. This request returns an HTTP 404 error: {"error":true,"reason":"Not Found"}.

Error 404 Meme not found

Registering the Routes

When you see a 404 error, it’s most likely because the functions weren’t registered with the Router in use, or the HTTP method used doesn’t match the registered method. You need to make UserController conform to RouteCollection so you can register it in the routes configuration. Open UserController.swift and add the following at its end:

extension UserController: RouteCollection {
  func boot(routes: RoutesBuilder) throws {
    // 1
    let usersRoute = routes.grouped("users")

    // 2
    usersRoute.post("send-verification-sms", use: beginSMSVerification)
    usersRoute.post("verify-sms-code", use: validateVerificationCode)
  }
}

The code above has two short steps:

  1. First, it groups the routes under the users path. This means that all routes added to usersRoute will be prefixed by users — for example, https://your-server.com/users/send-verification-sms.
  2. Then it registers two HTTP POST endpoints, providing each endpoint with one of the handler methods you defined above.

Now, open routes.swift and add this line inside the only existing function. This function registers your app’s routes:

try app.register(collection: UserController())

Calling the First API

Build and run your project again and try the previously failing curl command by pressing the up arrow key followed by Enter. You’ll get the following response with a new UUID:

{
  "attemptId": "477687D3-CA79-4071-922C-4E610C55F179",
  "phoneNumber": "+1234567890"
}

This response is your server saying that sending the SMS succeeded. Check for the message on your phone.

SMS verification message

Excellent! Notice how the sender ID you used in the initialization of AWSSNSSender is working correctly.

Calling the Second API

Now you’re ready to test the second part of the authentication: verifying the code. Take the attemptId from the previous request, the phone number you used in the previous step, and the code you received and place them into the following command. Then run the command in Terminal:

curl -X "POST" "http://localhost:8080/users/verify-sms-code" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{"phoneNumber": "+1234567890", "attemptId": "<YOUR_ATTEMPT_ID>", "code": "123456" }'

If you replaced each parameter correctly, the request will return an object with three properties: the status, the user object and a session token:

{
  "status": "ok",
  "user": {
    "id": "31D39FAD-A0A9-46E7-91CF-AEA774EA0BBE",
    "phoneNumber": "+1234567890"
  },
  "sessionToken": "lqa99MN31o8k43dB5JATVQ=="
}

Mission accomplished! How cool is it to build this yourself, without giving up on your users’ privacy or adding big SDKs to your client apps?

Build on my own meme

Where to Go From Here?

Download the completed project files by clicking the Download Materials> button at the top or bottom of this tutorial.

Save the session token in your client apps as long as the user is logged in. Check out Section III: Validation, Users & Authentication of the Server-Side Swift with Vapor book to learn how to use the session token to authenticate other requests. The chapters on API authentication are particularly helpful.

You can also read the documentation of Vapor’s Authentication API to better understand where you should add the session token in subsequent requests.

Do you want to continue improving your SMS authentication flow? Try one of these challenges:

  • Start using a PostgreSQL or MySQL database instead of in-memory SQLite and make changes to your app accordingly.
  • To avoid privacy issues and security breaches, hash phone numbers before saving and querying them, both in the User and the SMSVerificationAttempt models.
  • Think of ways to improve the flow. For example, you could add a isValid Boolean to make sure the code is only used once, or delete the attempt upon successful verification.
  • Implement a job that deletes expired and successful attempts.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!