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

There are many reasons why you’d want to verify your app’s users and identify them by phone number. SMS-based authentication is one of the options for a quick login experience that doesn’t require remembering passwords.

Nowadays, there are many services that provide SMS — aka short message service — authentication on your behalf. Using one might save you some time writing backend code, but it adds another dependency to your server and all your clients.

Writing your own solution is simpler than you think. If you already have a Vapor server for your app, or if you want to build a microservice for it, then you’ve come to the right place!

In this tutorial, you’ll learn how to build your own SMS authentication with Vapor and Amazon Web Services’ SNS. SNS, or Simple Notification Service, is the AWS service for sending messages of various types: push, email and of course, SMS. It requires an AWS account and basic knowledge of Vapor and Swift.

By the end of this tutorial, you’ll have two HTTP APIs that will allow you to create a user for your app.

Getting Started

Note: A local installation of Vapor and a set of AWS keys for SNS are prerequisites for this tutorial. Create a free AWS account and follow the basic SNS service setup in the IAM (Identity and Access Management) console. By the end of setup, you’ll have an Access Key ID and Secret Access Key that you’ll use to complete the tutorial.

Download the materials for this tutorial using the Download Materials button at the top or bottom of this page. Navigate to the materials’ Starter directory in your favorite terminal application and run the following command:

open Package.swift

Once your project is open in Xcode, it’ll fetch all the dependencies defined in the manifest. This may take a few minutes to complete. Once that’s finished, build and run the Run scheme to make sure the starter project compiles. As a last step before you start coding, it’s always a great idea to browse through the starter project’s source code to get a sense of the layout and various pieces.

How SMS Auth Works Behind the Curtain

You’ve most likely used an app with SMS authentication before. Insert your phone number, move to another screen, enter the code received in the SMS and you’re in. Have you ever thought about how it works behind the scenes?

If you haven’t, fear not: I’ve got you covered!

  1. The client asks the server to send a code to a phone number.
  2. The server creates a four- or six-digit code and asks an SMS provider to deliver it to the phone number in question.
  3. The server adds an entry in the database associating the sent code with the phone number.
  4. The user receives the SMS and inputs it in the client.
  5. The client sends the code back to the server.
  6. The server queries the database for the phone number and tries to match the code it saved before to the code it received.
  7. If they match, the server looks in the database to see if a user is associated with the phone number. If it doesn’t find an existing user, it creates a new one.
  8. The server returns the user object, along with some sort of authentication token, to the client.

You can see the steps detailed above in this diagram:

SMS auth step-by-step diagram

Note: Although the diagram represents a database connection over a network, the sample project uses an in-memory SQLite database for learning purposes. This isn’t intended for production use, as this database can’t be shared across multiple server instances and doesn’t persist data between launches. Refer to the Vapor documentation or the Server-Side Swift with Vapor book to learn how to improve data persistence with a production-ready database configuration.

Interacting With AWS SNS

To execute step two in the diagram above, you’ll need to create a class that asks SNS to send the text message. In the SourcesApp folder, create a new Swift file named SMSSender.swift. Make sure you’re creating this file in the App target. Next, add the following:

import Vapor
// 1
protocol SMSSender {
  // 2
  func sendSMS(
    to phoneNumber: String,
    message: String,
    on eventLoop: EventLoop) throws -> EventLoopFuture<Bool>
}

There are a few things to notice here:

  1. You define a protocol called SMSSender, which creates an abstraction around sending an SMS. This means it can potentially be used to create many classes, each with its own mechanism for SMS delivery.
  2. sendSMS(to:message:on:) receives a destination phone number, a text message and the current EventLoop, and it returns an EventLoopFuture<Bool>. This is a future value that indicates if sending the message succeeded or failed. You can learn more about EventLoopFuture and asynchronous programming in this article or Vapor’s documentation.

Next, you’ll create the class that implements this protocol. Under the SourcesApp folder, create a file named AWSSNSSender.swift and add the following code to it:

import Vapor
import SNS

class AWSSNSSender {
  // 1
  private let sns: SNS
  // 2
  private let messageAttributes: [String: SNS.MessageAttributeValue]?

  init(accessKeyID: String, secretAccessKey: String, senderId: String?) {
    // 3
    sns = SNS(accessKeyId: accessKeyID, secretAccessKey: secretAccessKey)

    // 4
    messageAttributes = senderId.map { sender in
      let senderAttribute = SNS.MessageAttributeValue(
        binaryValue: nil,
        dataType: "String",
        stringValue: sender)
      return ["AWS.SNS.SMS.SenderID": senderAttribute]
    }
  }
}

This is the class definition and initialization. Here’s an overview of what the code above does.

  1. This keeps a private property of the SNS class. This class comes from the AWSSDKSwift dependency declared in Package.swift. Notice that in the second line, you need to import the SNS module.
  2. SNS allows setting specific message attributes. You’re interested in SenderID so that the SMS messages arrive with the sender name of your app. The class will use messageAttributes whenever a message is sent as part of the payload.
  3. The initializer receives your AWS access key and the matching secret. You pass these on to the SNS class initializer.
  4. The initializer may also receive an optional senderId. Use the map method on the
    Optional argument to map it to the messageAttributes dictionary. If senderId is nil, messageAttributes will also be nil. If it has a value, map will transform the string into the needed dictionary.

For security, and to allow for easier configuration, don’t hardcode your AWS keys into your app. Instead, a best practice is to use environment variables. These variables are set in the environment in which the server process runs, and they can be accessed by the app at runtime.

To add environment variables in Xcode, edit the Run scheme:

You can also edit the current scheme by typing Command + Shift + ,

Edit the run scheme in Xcode

Then, select the Arguments tab. Under Environment Variables, click the + button to add a new variable.

You’ll need two variables: AWS_KEY_ID and AWS_SECRET_KEY. Add the corresponding value for each one:

Add the values of the variables in Xcode.

Add environment variables in Xcode
Note: You’ll need these environment variables when deploying your server to staging/production as well. Check Section V: Production & External Deployment in the Server-Side Swift with Vapor book to learn more about environment variables during deployment.

Next, add an extension below the code you just wrote to make AWSSNSSender conform to the SMSSender protocol:

extension AWSSNSSender: SMSSender {
  func sendSMS(
    to phoneNumber: String,
    message: String,
    on eventLoop: EventLoop) throws -> EventLoopFuture<Bool> {
    // 1
    let input = SNS.PublishInput(
      message: message,
      messageAttributes: messageAttributes,
      phoneNumber: phoneNumber)

    // 2
    return sns.publish(input).hop(to: eventLoop).map { $0.messageId != nil }
  }
}

This protocol conformance is straightforward. It delegates the request to publish a message to the AWS SNS service like so:

  1. First, you create a PublishInput struct with the message, the attributes created in the initialization and the recipient’s phone number.
  2. Next, you ask the SNS instance to publish the input. Because it returns an EventLoopFuture<PublishResponse> in another EventLoop, use hop(to:) to get back to the request’s event loop. Then map the response to a Boolean by making sure its messageId exists. The existence of the messageId means that the message has been saved and Amazon SNS will try to deliver it.

Finally, you still need to initialize an instance of AWSSNSSender and register it in the configuration. In Vapor 4, services can be registered to the Application instance using storage. Open SMSSender.swift and add the following code:

// 1
private struct SMSSenderKey: StorageKey {
  typealias Value = SMSSender
}

extension Application {
  // 2
  var smsSender: SMSSender? {
    get {
      storage[SMSSenderKey.self]
    }

    set {
      storage[SMSSenderKey.self] = newValue
    }
  }
}

To allow registering a service, you need to:

  1. Declare a type that conforms to StorageKey. The only requirement is having a typealias for the type of the value you’ll store — in this case, a SMSSender.
  2. Extending Application, add a property for SMSSender and implement the getter and the setter, which each use the application’s storage.

Now it’s time to initialize and register the service. Open configure.swift and add this block of code after try app.autoMigrate().wait():

// 1
guard let accessKeyId = Environment.get("AWS_KEY_ID"),
      let secretKey = Environment.get("AWS_SECRET_KEY") else {
  throw ConfigError.missingAWSKeys
}

// 2
let snsSender = AWSSNSSender(
  accessKeyID: accessKeyId,
  secretAccessKey: secretKey,
  senderId: "SoccerRadar")
// 3
app.smsSender = snsSender

Here’s what you’re doing in the code above:

  1. You retrieve the AWS keys from your environment variables, throwing an error if your app can’t find them.
  2. You initialize AWSSNSSender with those keys and the app’s name. In this case, the name is SoccerRadar.
  3. You register the snsSender as the application’s SMSSender. This uses the setter you defined in the Application extension in the previous code block.

Once you have the sender configured, initialized and registered, it’s time to move on to actually using it.