HTTP Basic Authorization in Vapor 3

755 Views

This tutorial will cover how to authenticate users with an email/username and password using HTTP Basic Authentication in Vapor 3.

Conform your User Model to the BasicAuthenticatable Protocol

Suppose you have a User.swift, such as:

    final class User: PostgreSQLModel {
        var id: Int?
        var email: String
        var password: String
    
        init(email: String, password: String) {
            self.email = email
            self.password = password
        }
    }

The first thing we need to to, is to conform to the BasicAuthenticatable protocol, like so:

    extension User: BasicAuthenticatable {
        static var usernameKey: UsernameKey { return \User.email }
        static var passwordKey: PasswordKey { return \User.password }
    }

We have to conform to this protocol, because the Auth package needs to know which properties on User it should use to authenticate against. For example, some people may want to authenticate using user.username instead of user.email, as I am doing here.

Extend our User class with Structs

Create the following structs that will represent the data we wish to receive from the login request, as well as the data we want to pass back to the authenticated client:

    extension User {
        struct AuthenticatedUser: Content {
            var email: String
            var id: Int
        }
    
        struct LoginRequest: Content {
            var email: String
            var password: String
        }
    }

AuthenticatedUser is the struct which will contain the data we want to send back to the user once they are authenticated. We cannot simply use an instance of our User class, because then it will also pass back the hashed password along with it, and that is insecure. We create the LoginRequest struct to decode the JSON data our application will accept when the user logs in with their credentials.

Controller

Now, in our controller, we can add the following methods to create and log in a user:

    final class UserController {
        func createUser(_ request: Request) throws -> Future { // 1
            let futureUser = try request.content.decode(User.self) // 2

            // 3
            return futureUser.flatMap(to: User.AuthenticatedUser.self) { (user) -> Future in
                let hasher = try request.make(BCryptDigest.self)
                let passwordHashed = try hasher.hash(user.password)
                let newUser = User(email: user.email, password: passwordHashed) // 4
                return newUser.save(on: request).map(to: User.AuthenticatedUser.self) { authedUser in // 5
                    return try User.AuthenticatedUser(email: authedUser.email, id: authedUser.requireID()) // 6
                }
            }
        }
  
        func loginUser(_ request: Request) throws -> User.AuthenticatedUser {
            let user = try request.requireAuthenticated(User.self) // 7
            return try User.AuthenticatedUser(email: user.email, id: user.requireID()) // 8
        }
    }

That's quite a lot of code, let's explain it some:

  • 1: We want our function that creates a user to return us a Future
  • 2: We decode the request. Note, this still represents a future user -- it's not an actual User object yet, so we don't have access to user.email, user.password, etc.
  • 3: We call .flatMap on our futureUser object, and this gives us access to an actual User object inside the closure. Note though, that with flatMap, the closure must still return a future--not an instance of User
  • 4: The password came through from the user as plaintext, but before we save it to the database, we need to hash it, and then create a new User object that's identical to the last one--except with a hashed password, not a plaintext one.
  • 5: We save our newUser object, and then call map on the result of the save call. We call .map here, because we want the closure to return an instance of User.AuthenticatedUser, not a future. Remember the rule: return a future: use flatMap. return a non-future: use map.
  • 6:We return the instance of User.AuthenticatedUser
  • 7: This authenticates the user, and returns an instance of User. Note, we don't have to return a future here, since try request.requireAuthenticated(User.self) already returns to us an instance of User
  • 8: We return our instance of User.AuthenticatedUser

import Authentication and Register AuthenticationProvider

In configure.swift, add import Authentication and add the following line where you register your services:

try services.register(AuthenticationProvider())

You will need to add import Authentication to your User.swift and UserController.swift as well.

Add the Middleware

The last step, is we must add the middleware to our routes. Add the following to your routes:

let userController = UserController()
    router.post("createUser", use: userController.createUser) // 1
    let middleWare = User.basicAuthMiddleware(using: BCryptDigest()) // 2
    let authedGroup = router.grouped(middleWare) // 3
    authedGroup.post("login", use: userController.loginUser) // 4
  • 1: The createUser route should be public
  • 2: we create the authentication middleware (which does a lot of the heavy lifting for us) using a BCyptDigest as our password verifier
  • 3: we create a route group with this middle ware
  • 4: lastly, we create the login route from the authedGroup. All routes grouped under authedGroup will now require a username/email and base64 encoded password be sent in the Authorization Header. (in the format: Authorization Basic YW5hcGFpfdeDfgfM6czc3vcmQ=)

That's all for this tutorial. Plenty more to come :) Happy coding!



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