Vapor 4 Authentication: Getting Started

In this Vapor 4 tutorial, you’ll learn how to implement user authentication for your app using both bearer tokens and basic authentication headers. By Natan Rolnik.

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

Creating Tokens for a User

Next, you need to make it possible to create a new token for a given user.

Below User.create(from:), add the following method:

// 1
func createToken(source: SessionSource) throws -> Token {
  let calendar = Calendar(identifier: .gregorian)
  // 2
  let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date())
  // 3
  return try Token(userId: requireID(),
    //4
    token: [UInt8].random(count: 16).base64, source: source,
    expiresAt: expiryDate)
}

Here’s what you’re doing in this function:

  1. This is a throwing function (more on that in step 3) on User, which receives a SessionSource and returns a new Token.
  2. You use a Calendar to generate a date a year ahead of the current date, which you’ll use as the expiry date. Change this line if you want a shorter or longer expiry date.
  3. Using the initializer you created in the previous section, you generate the token. requireID() throws an error when the user isn’t in the database and doesn’t have an ID yet.
  4. Generate the token value itself by creating 16 random bytes and getting the token’s Base 64-encoded string representation.

Including the Token in the Response

Once you’ve implemented createToken(source:), it’s time to activate it in the user controller.

Go back to Controllers/UserController.swift‘s create(req:). Add a declaration for the token below the user property:

var token: Token!

Now, replace the .flatMapThrowing closure after return user.save(on: req.db) with the following:

.flatMap {
  // 1
  guard let newToken = try? user.createToken(source: .signup) else {
    return req.eventLoop.future(error: Abort(.internalServerError))
  }
  // 2
  token = newToken
  return token.save(on: req.db)
}.flatMapThrowing {
  // 3
  NewSession(token: token.value, user: try user.asPublic())
}

This is what you’re doing in this chunk of code:

  1. You create a new token from the fresh user you just saved. If it fails, it throws an error.
  2. You keep a reference to the token and save it.
  3. Upon completion, you initialize the NewSession with the token value and the User.Public from the user. The User.Public struct is a technique used to send information to the clients without exposing fields that are internal to the server.

It’s finally time to test the first API you created with this tutorial!

Build and run, then open the Paw or Postman API file from this tutorial’s download materials. Select the (1) Sign up request, set the server URL to localhost:8080, then send the request.

You’ll get a JSON response that includes both the token and the user:

{
  "token": "t+oHBUwU2Rv+qp7O2Ed0UQ==",
  "user": {
    "username": "NatanTheChef",
    "id": "138191B9-445D-442D-9F70-B858081A661B",
    "updated_at": "2020-03-07T19:40:54Z",
    "created_at": "2020-03-07T19:40:54Z"
  }
}
Note: Your token and id values will be different.

Congratulations! You’ve created a new user and token, and returned that token in your server response. Next, you’ll use that token to authenticate the user.

Authenticating the User With a Token

There are two ways to confirm the server saved the user: The first is to use the SQLite browser by opening the users table to see the new record — select the users table, then select the Browse Data tab. The second is to add an endpoint allowing users to fetch their own information.

Your next step will be to add that endpoint.

Supporting Basic Authentication on the User Model

In order for Vapor to know that a User instance can be used for authentication, you need to conform User to ModelAuthenticatable. Vapor uses this protocol to perform all the steps around authenticating a user with a username and a password in the request headers and to link it to tokens.

Open User.swift and add the following extension:

extension User: ModelAuthenticatable {
  // 1
  static let usernameKey = \User.$username
  static let passwordHashKey = \User.$passwordHash
  
  // 2
  func verify(password: String) throws -> Bool {
    try Bcrypt.verify(password, created: self.passwordHash)
  }
}

Fluent’s ModelAuthenticatable protocol is succinct. First, it requires two KeyPaths, where the value is a field of type String. This tells Vapor which fields to look for when querying the user upon authentication.

Secondly, it requires your user model to implement verify(password:), which determines whether the received password matches the password hash and returns the result. As with the registration, this function also uses Bcrypt to perform this check.

Conforming a Token to the ModelTokenAuthenticatable Protocol

Next, Vapor needs to know that it can use the Token for authenticating users. This lets it provide the authentication middleware, which is able to find the user for a token supplied in the request authentication header.

Open Models/Token.swift and add this extension:

extension Token: ModelTokenAuthenticatable {
  //1
  static let valueKey = \Token.$value
  static let userKey = \Token.$user

  //2
  var isValid: Bool {
    guard let expiryDate = expiresAt else {
      return true
    }
    
    return expiryDate > Date()
  }
}

Fluent’s ModelTokenAuthenticatable protocol is also very concise. First, it needs two KeyPaths — one for the token value field and another for the user relationship. Next, it checks the token to see if it’s valid at a specific moment. If your tokens don’t expire, simply return true.

In this case, you take expiresAt and compare it to the current date. If the current date is later than the expiry date, it’s invalid, causing Vapor to delete the token from the database and return 401 unauthorized.

Adding the Me Endpoint

Now Vapor can provide the token authenticator middleware. Go back to UserController.swift and add the following lines to the end of boot(routes:):

let tokenProtected = usersRoute.grouped(Token.authenticator())
tokenProtected.get("me", use: getMyOwnUser)

The first line creates a router using the /users path defined in the first line of this method, and also wraps them in an authentication middleware. This means that every request going through the middleware requires an authenticated user.

The second line makes the /users/me endpoint use getMyOwnUser(req:). Scroll down to this function and replace the thrown error with the following line:

try req.auth.require(User.self).asPublic()

This returns the user information by accessing the request’s authentication cache, fetches the user who’s performing the request and converts it to a public user. If the request isn’t authenticated, then it throws a 401 unauthorized error.

Whenever you need to get the current user from within a request, simply use req.auth.require(User.self), as long as the request came through an authentication middleware, as shown above.
Build and run, then send the (2) Me request in the API file. The response should contain the user object you just signed up:

{
  "username": "NatanTheChef",
  "id": "138191B9-445D-442D-9F70-B858081A661B",
  "updated_at": "2020-03-07T19:40:54Z",
  "created_at": "2020-03-07T19:40:54Z"
}
Note: If you get an Unauthorized error, run the Sign up request again, to reset the token.