Token Authentication in Vapor 3

1987 Views

This tutorial is a continuation of the previous tutorial, which can be found here.

This tutorial will begin where the last one left off, and you can download the project for beginning this tutorial here

For those who did not complete the first short tutorial, just to catch you up to speed:

  • We have a User model, with properties of username and password
  • We have a UserController, with actions for creating a user and logging in
  • I'm using Postgres as my database

Ok, on to Token Auth!

Add the Token Model

First, add the Token model. Create a new class called Token, like so:

    final class Token: PostgreSQLModel {
        var id: Int?
        var token: String
        var userId: User.ID
    
        init(token: String, userId: User.ID) {
            self.token = token
            self.userId = userId
        }
     }

Next, add the following convenience method on Token to retrieve a user for a given Token:

    extension Token {
        var user: Parent {
            return parent(\.userId)
        }
    }

Add the Authentication Protocols

There are 2 protocols that our Token model must conform to: Authentication.Token, and BearerAuthenticatable.

And, there is one additional protocol that our User model must conform to: `TokenAuthenticatable`.

First, let's conform our User model to TokenAuthenticatable, with the following:

    extension User: TokenAuthenticatable { typealias TokenType = Token }

Next, let's conform our Token to BearerAuthenticatable:

    extension Token: BearerAuthenticatable {
        static var tokenKey: WritableKeyPath { return \Token.token }
    }

We have to add this static property so that the Authentication package knows which property on our Token model we are going to use as our token.

And then, conform to Authentication.Token:

    extension Token: Authentication.Token {
        static var userIDKey: WritableKeyPath { return \Token.userId } // 1
        typealias UserType = User // 2
        typealias UserIDType = User.ID //3
    }

At 1, we tell the Authentication package which property to use for our userId.

At 2, we tell it the name of the class to use for our user (in case yours isn't named "User")

And at 3, we tell it which property on our User class it needs in order to identify a user, given a specific token

Now, we have everything setup for us, just a few things left to do:

  • Add a PublicUser struct so we can return a custom User object with the user's username and token (not username and password!)
  • Edit the createUser method to create a new token for us upon creation of our account
  • Add a helper function to generate a random token for us
  • Lastly, test our application with some protected routes!

Add the PublicUser struct

First, let's create the PublicUser struct. Inside your User class, add the following:

    struct PublicUser: Content {
        var username: String
        var token: String
    }

This will allow us to pass back to a user their username and token, instead of their username and password. We can do so by simply returning Future in our route handlers, instead of Future.

Edit the createUser method in the UserController

Replace the createUser method in UserController.swift with the following implementation:

    func createUser(_ request: Request) throws -> Future { // 1
        return try request.content.decode(User.self).flatMap(to: User.PublicUser.self) { user in // 2
            let passwordHashed = try request.make(BCryptDigest.self).hash(user.password)
            let newUser = User(username: user.username, password: passwordHashed)
            return newUser.save(on: request).flatMap(to: User.PublicUser.self) { createdUser in
                let accessToken = try Token.createToken(forUser: createdUser) // 3
                return accessToken.save(on: request).map(to: User.PublicUser.self) { createdToken in // 4
                    let publicUser = User.PublicUser(username: createdUser.username, token: createdToken.token)
                    return publicUser // 5
                }
            }
        }
    }

That's quite a bit of code, let's explain it below:

  • At 1, we are now returning Future instead of Future
  • At 2, we change our flatMap call to flatMap(to: User.PublicUser.self). This is propagated throughout the function
  • At 3, we create a new token for a user (this method hasn't been created yet)
  • At 4, we save the new token, and inside the route closer we have access to our newly created token.
  • At 5, we use the createdToken, then create and return an instance of User.PublicUser

Add method to create token

Add the following static function to your Token class:

    static func createToken(forUser user: User) throws -> Token {
        let tokenString = Helpers.randomToken(withLength: 60)
        let newToken = try Token(token: tokenString, userId: user.requireID())
        return newToken
    }

Then, create a Helpers class with the following function to create the random token string:

    class Helpers {
        class func randomToken(withLength length: Int) -> String {
            let allowedChars = "$!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
            let allowedCharsCount = UInt32(allowedChars.count)
            var randomString = ""
            for _ in 0..< length {
                let randomNumber = Int(arc4random_uniform(allowedCharsCount))
                let randomIndex = allowedChars.index(allowedChars.startIndex, offsetBy: randomNumber)
                let newCharacter = allowedChars[randomIndex]
                randomString += String(newCharacter)
            }
            return randomString
        }
    }

Add the Middleware and Test

Ok great! now you should be setup to be able to authenticate with tokens. To test, first add the following to your routes.swift:

    let tokenAuthenticationMiddleware = User.tokenAuthMiddleware() // 1
    let authedRoutes = router.grouped(tokenAuthenticationMiddleware) // 2
    authedRoutes.get("this/protected/route") { request -> Future in
        let user = try request.requireAuthenticated(User.self) // 3
        return try user.authTokens.query(on: request).first().map(to: User.PublicUser.self) { userTokenType in // 4
            guard let tokenType = userTokenType?.token else { throw Abort.init(HTTPResponseStatus.notFound) }
            return User.PublicUser(username: user.username, token: tokenType) // 5
        }
    }

Here, at 1, we create the middleware given to use by the Authentication package when our User class conforms to TokenAuthenticatable.

At 2, we create a group of routes using this middleware. All routes created with this middleware will require a valid token be sent in the authorization header.

At 3, we call request.requireAuthenticated, and this method returns to us a User object (not a future). For that reason, in the route handler we do not have to return a Future

At 4, we retrieve the user's auth tokens, and grab the first one. The result of the call user.authTokens.query(on: request) is a query builder, so we could also do some custom filtering as well if needed. For simplicity though, here we will just assume the getting the first one is good. In a real world application, you may want to filter by date, or maybe you have an isExpired property on your tokens to query against.

And finally at 5, we return an instance of PublicUser, passing the auth token and username back to the client.

Now, use a REST client, such as Postman, to make a POST request to http://localhost:8080/createUser, with parameters like:

    {username: "anapaix", password: "password"}

And you should get a response:

    {
        "token": "QUKrHOkCDabXNQ8ipPW8kpD87mEarceKPaSNU8gSeCI662Y!FXoj6N2NF",
        "username": "anapaix"
    }

Then, make a get request to http://localhost:8080/this/protected/route, and for the Authorization header, add Bearer YourTokenHere, such as:

    Bearer QUKrHOkCDabXNQ8ipPW8kpD87mEarceKPaSNU8gSeC&I662Y!FXoj6N2NF

And you can access the protected route! Try accessing the route without the header, or with an incorrect token as well, and you'll see that it's inaccessible.

You can get the final code for this project on Github, here.

Thanks for reading, and happy coding!



Enjoy this article? Consider supporting VaporForums by following us on Twitter! Get the latest Vapor articles, tutorials, and news.