Basic HTTP Authentication in Vapor 2

584 Views

This tutorial will cover basic HTTP Authentication in vapor 2, and use vapor's PasswordAuthenticationMiddleware to protect routes so they are only accessible while authenticated.

To start, download the starter project on GitHub here

The starter project includes:

  • Vapor's LeafProvider package (we will be using Leaf for views)
  • A login and register form as .leaf files.
  • Vapor's AuthProvider package, used to aid in authentication
  • No models, no controllers ( we will build these)
  • No database driver. Read my tutorial here on how to add Postgres as a db driver to persist data in your app.

Let's get started!

Part 1: Create the model

Create a new file, under the Models folder, and call it User.swift. Copy and paste the code from this gist in it. This user model is very basic, with fields for storing an email/password. As of now, you will receive a few errors after the copy/paste and can not build. Let's fix that.

In User.swift, outside the main class definition, add the following extension:

    extension User: PasswordAuthenticatable {
        // here, we will conform to the PasswordAuthenticatable protocol
    }

Then add the following import statement:

    import BCrypt

Now, inside extension User: PasswordAuthenticatable, add the following lines of code:

    public static let hasher = BCryptHasher(cost: 11) // (1)
    public static let passwordVerifier: PasswordVerifier? = User.hasher (2)
    
    (3)
    var hashedPassword: String? {
        return password.makeString()
    }

(1) is the hasher we will use. Obviously, we never store plain text passwords. When the user enters a password upon registration, we will pass that password through 1-way hash, and then store the hash. This isn't a tutorial on Bcrypt, but there is a good and basic discussion [here](https://stackoverflow.com/questions/25586073/bcrypt-what-is-meant-salt-and-cost) about what a "cost" is in Bcrypt. The ELI5 version is a higher cost is more secure, but you don't want too high a cost, because then saving/authenticating takes too long on your server. For example, I tested locally with a cost of 15, and saving a new user took about ~8-10 seconds, which obviously in a real world app is way too long.

(2) and (3) are to be used internally by the PasswordAuthenticatable and PasswordAuthenticationMiddleware when determining if you can access authenticated routes. We won't use them directly, but if you remove them, this will not work.

Part 2: Create the controller

Next, let's create our controller. Under the Controllers folder, create a new file, and call it UserController. Next, add the following code to it:

    import Foundation
    import Vapor

    final class UserController {
      let viewRenderer: ViewRenderer
      let droplet: Droplet
    
      init(view: ViewRenderer, drop: Droplet) {
        self.viewRenderer = view
        self.droplet = drop
      }
    
      // this method will be called to add routes for showing the registerForm
      // showing the loginForm, 
      // and for submitting the POST requests for logging in and registering a user
      func addRoutes() {
        droplet.get("registerForm", handler: showRegisterForm)
        droplet.post("submitRegistration", handler: register)
        droplet.get("showLoginForm", handler: showLoginForm)
        droplet.post("login", handler: login)
      }

      func login(request: Request) throws -> ResponseRepresentable {
        return try JSON(node: ["test"])
      }
    
      func showLoginForm(request: Request) throws -> ResponseRepresentable {
        let view = try viewRenderer.make("user/loginForm", ["":""], for: request)
        return view
      }
    
      func register(request: Request) throws -> ResponseRepresentable {
        return try JSON(node: ["test"])
      }
    
      func showRegisterForm(request: Request) throws -> ResponseRepresentable {
        let x = try viewRenderer.make("user/registerForm", ["":""], for: request)
        return x
      }
    }

Now, go to Routes.swift, and inside the setupRoutes() function, add the following code:

    let userController = UserController(view: self.view, drop: self)
    userController.addRoutes()

Part 3: Edit the controller for registering and logging in

Start your server, and navigate to http://localhost:8080/registerForm now, and you should see the register form. Attempting to register now will not do anything, we need to add code to our /register POST route to get the values for the fields, and then save them on the user. In the register function in UserController.swift, add the following code:

    // first, we get the email and password as strings out of the parameters of the request
    if let email = request.data[User.emailKey]?.string, let password = request.data[User.passwordKey]?.string {
          // first, we take the password string, and convert it to bytes, using our bcrypt hasher
          let passwordHashed = try User.hasher.make(password.makeBytes())
          // init our user with an email and password
          let user = try User(email: email, password: passwordHashed)
          // try to save the user. If this fails, you will receive a response saying ["error"], and the `return user` below will not be ran
          try user.save()
          return user
      }
        return try JSON(node: ["error"])  // to return in case of error

Now, go to Config+Setup.swift and add preparations.append(User.self) to the setupPreparations() method.

Next, start your server and navigate to http://localhost:8080/registerForm. Fill in an email and password. It should succeed and you should get a response similar to:

    {"id":1,"email":"yourEmailHere"}

Now that we have registered a user, let's go through the login process. In UserController.swift, in the login function, add the import statement:

    import AuthProvider

Next, in the login function, replace its contents with the following code:

    // again, get the email and password strings from the POST request
    if let email = request.data[User.emailKey]?.string, let password = request.data[User.passwordKey]?.string {
        // we do not need to hash the password before creating the credentials object. The Password() function does this for us
         let credentials = Password(username: email, password: password)
         // try to authenticate the user with the given credentials. If it fails, it will skip the the return statement at the bottom
         let user = try User.authenticate(credentials)
         return try JSON(node: ["user": user])
    }
     return try JSON(node: ["error": "invalid credentials"]) // return in case of error

Now, restart your server, and create a user as above (don't forget--if you are not using a db driver, you will have to create a new user after every server restart, as data will not be persisted). Register a user, then navigate to http://localhost:8080/showLoginForm. Enter your username and password that you registered. It should show a success JSON like:

    {"user":{"id":1,"email":"yourEmailHere"}}.

If there is an error, you will get a 401 Unauthorized page.

Part 4: Add the middleware

So we can register and login a user, but how can we protect certain routes that we want visible to only logged in users? With the PasswordAuthenticationMiddle ware provided by Vapor.

In Routes.swift, add the import statement: import AuthProvider. In your setupRoutes() function, add the following:

    let passwordMiddleware = PasswordAuthenticationMiddleware(User.self)
    let authed = grouped(passwordMiddleware)

The first line initializes the middleware, and passed in our User model. The author object will be a group of routes that is protected by this middleware, and only request which contain the appropriate Authorization headers will be allowed access.

still in setupRoutes(), add the following:

    authed.get("protectedRoute") { req in
        return try JSON(node: ["success": "you're authenticated!"])
    }
    

Restart your server, and unauthenticated, navigate to `http://localhost:8080/protectedRoute`. You should receive a 401 Unauthorized page. Now, using an HTTP client, make a GET request to the protected route, but include an `Authorization` header. For example, mine looks like:

    Authorization: Basic bWlzdHk6cGFzc3dvcmQ=

The bWlzd... string there is my username:password base64 encoded (that is the standard for HTTP auth). So for a username of "misty" and a password of "password", the aforementioned string stands for: misty:password, but base64 encoded. We prepend "Basic" to the request as well, to indicate basic HTTP authentication. It must be in this exact format, or it will not work. Including this authorization header will allow you to access the protected route.

When authenticated, you should get the JSON response:

    {  "success": "you're authenticated!" }

Otherwise, you will receive a JSON response:

     {
      "identifier": "Authentication.AuthenticationError.invalidCredentials",
      "reason": "Authentication error: Invalid credentials",
      "debugReason": "Authentication error: Invalid credentials",
      "error": true
    }

And that's how you can register a user, log them in, and protect routes using the PasswordAuthenticationMiddleware in Vapor 2.0!



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