Routing in Vapor 2, Part 3: Securing Routes Using Middleware

1018 Views

The third part of this series will focus on securing routes with middleware.

Very brief intro to middleware

Oftentimes in an application, we want to restrict a user's access to certain routes. For example, if a user is already logged in, we don't want them to be able to access /login (or whatever your login route is).

Middleware, as its name might imply, allows us to catch requests in between the client and the server. When we intercept these requests, we can do different things on them--we can modify the request, we can add headers, we can remove headers, redirect the request, or abort it--middleware is very powerful. There is much, much more to middleware than we will go through in this post--here we will go through enough to use it for securing routes in our Vapor applications.

First create a new vapor project, called SecureAPI, by running

    vapor new SecureAPI --template=api

cd into the project, run vapor Xcode and open the project when it has been generated. Delete the PostController.swift and Post.swift, we will not be using them. In Config+Setup.swift, delete preparations.append(Post.self) from your preparations as well. Delete all content from your setupRoutes() function. Good! Our project is rid of all boilerplate vapor code, time to start fresh. Your setupRoutes() should look like:

     func setupRoutes() throws {
        
    }

In the following example, we will create middleware to check for a valid authorization token. It is worth noting that Vapor already has several middleware built in, such as their TokenAuthenticationMiddleware and their RedirectMiddleware, and PasswordAuthenticationMiddleware. In the event that you use those (I went over PasswordAuthenticationMiddleware in my tutorial on HTTP Auth, here), the process for applying middleware to routes is the same, the only difference is that in this example, we manually create middleware, and if you're using Vapor-provided middleware, you don't need to create it yourself.

Create our middleware

Create a new file by going to File -> New -> File, and call it APIMiddleware.swift. Add the following to the file:

     import HTTP
     import Vapor
     final class APIMiddleware: Middleware { // (1)
       // (2)
      func respond(to request: Request, chainingTo next: Responder) throws -> Response {
         let validToken = "w2$ly2$ByEITKwlyByEITK5BE20DIcvma5BE20DIcvma" // (3)
        
         guard let authHeader = request.headers["Authorization"]?.string else { // (4)
            throw Abort.unauthorized
         }
         if authHeader != validToken {
            throw Abort.unauthorized // (5)
        } else { return try next.respond(to: request) } // (6)
       }
     }

There is a lot going on here! let's go through it step by step:

  • (1) We conform our class to the Middleware protocol, to indicate this class can be used as middleware. All custom middleware we create must conform to the Middleware protocol.
  • (2) the respond(to: _ chainingTo: ) function is required for us to conform to the Middleware protocol. If you remove it, you'll get an error message. This is the function that we will use to intercept the request (the request we'll intercept is the request in the function parameter)
  • In (3) I create a valid token to compare the auth header with. In most cases, your value for such a token (or some other value to compare against) will come from the server. For convenience, and because this isn't a tutorial about creating API tokens, I will simply create it here as a string.
  • In (4) we check the Authorization header for the presence of a string. If none exists, we abort the request.
  • In (5) if there is a token, but it is not valid, then we return a 401 forbidden response.
  • In (6), if the token in the auth header is valid, we pass the request down to the next responder, which may be another middleware elsewhere defined in our application, or to our controller (in our case now, it is to the route closure for the final response). next.respond(to: request) is basically us saying "we're done here now, proceed with the HTTP request as normal."

Apply our middleware to routes

Add the following to your setupRoutes() function:

    let apiMiddleware = APIMiddleware() // (1)
    let apiProtected = grouped(apiMiddleware) // (2)
        
    apiProtected.get("hello") { request in // (3)
        return "You have accessed a protected route"
    }

Before we build and run for testing, let's go over a few things. The above is one way we apply middleware to routes. (You can also apply middleware to *every* route in your project by editing the droplet.json file, but we won't go into that here)

  • (1) First, we initialize our middleware class
  • (2) we create a group on our instance of Droplet. All routes in this group will pass through the APIMiddleware.
  • (3) We create a protected route.

Testing it

Time to test it! In your browser, navigate to http://localhost:8080/hello , and you will receive a 401 Unauthorized page, because we did not supply an authorization header with a valid token.

Now, using an HTTP client, make a GET request to http://localhost:8080/hello, and for the HTTP Header field "Authorization", supply the value w2$ly2$ByEITKwlyByEITK5BE20DIcvma5BE20DIcvma (the value of our valid token). The response will read: You have accessed a protected route. Change the authorization header to be an incorrect value again, and you will once again receive a 401 Unauthorized response.


A larger example

Now, technically this is all the knowledge you need to protect any route: Create middleware, make it pass whatever test (valid token, etc) in order to proceed, and then apply it to your route. However, keep reading for a slightly more fully-baked example.

Create a file called Pet.swift, and in it copy and paste the following boilerplate code from this gist for creating a Pet model. Then, create another file, PetController.swift and add the following code to it:

    import Foundation
    import Vapor

    final class PetController {
        let droplet: Droplet
    
        init(drop: Droplet) {
            self.droplet = drop
        }
        func addRoutes() {
    
        }
    }

Before we edit our addRoutes() function, let's think about the setup we will want:

  • We want 3 routes: to create a Pet, view all Pets, and destroy a Pet object
  • We want anyone to be able to view all Pets
  • We want only authenticated users to be able to create a Pet (authentication will be simulated as above, with a valid token variable)
  • Someone with an admin role can create, view all, and destroy a Pet object.
  • No other roles can destroy a Pet object

Add preparations.append(Pet.self) to your setupPreparation() function in Config+Setup.swift.

Create AdminMiddleware

Create a new class, called AdminMiddleware, and place the following in it:

     final class AdminMiddleware: Middleware {
     func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        let adminUsername = "admin"
        guard let username = request.data["username"]?.string else {
            throw Abort.unauthorized
        }
        if username != adminUsername {
            throw Abort.unauthorized
        } else { return try next.respond(to: request) }
      }
    }

Very similar to before, we will check if the data is valid. This time, we are checking for the "username" key in the request.data to see if it matches. It is worth noting here that, instead of throwing Abort.unauthorized here, you could also redirect the request if you want, using return Response(redirect: "/your/path/here"), but here, for simplicity, we will just throw an error.


Add methods in Controller class

In PetController.swift, add the following functions for deleting, creating, and viewing Pets:

    func delete(request: Request) throws -> ResponseRepresentable {
        let pet = try request.parameters.next(Pet.self)
        try pet.delete()
        return Response(status: .ok)
    }
    
    func create(request: Request) throws -> ResponseRepresentable {
        guard let json = request.json else { throw Abort(.badRequest, reason: "no json")}
        let pet = try Pet(json: json)
        try pet.save()
        return pet
    }
    
    func viewAll(request: Request) throws -> ResponseRepresentable {
        return try Pet.all().makeJSON()
    }

Setup our restricted routes

Next, let's restrict access to our routes. Change the addRoutes() function to read:

    func addRoutes() {
        droplet.get("viewall", handler: viewAll) // (1)

        let apiMiddleware = APIMiddleware() // (2)
        let apiProtected = droplet.grouped(apiMiddleware)
        apiProtected.post("create", handler: create)
        
        let adminMiddleware = AdminMiddleware() // (3)
        let adminProtected = apiProtected.grouped(adminMiddleware) // (4)
        adminProtected.delete("delete", Pet.parameter, handler: delete)
    }
  • In (1), we create the /viewall route. This is not protected a protected route.
  • In (2) We apply our APIMiddleware to our route for creating Pets. Remember, we only want authorized users to create pets.
  • In (3), we add our AdminMiddleware.
  • In (4), notice that we create our adminProtected group based off the apiProtected group. This is because we can chain middleware together. So now our adminProtected group will also search for a valid API token as well.

Then, in your setupRoutes() function in Routes.Swift, add:

    let petControler = PetController(drop: self)
    petControler.addRoutes()

And that's it! All of our routes are now appropriately protected. You can find the finished tutorial [here](https://github.com/JoeyBodnar/SecureAPI). Questions/comments welcome below!



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