Tutorial: How to write a Middleware
This tutorial will teach you how to write your own middleware. It’s basically a function that gets executed before the request arrives in your controller. It enables you to do powerful things. We’ll write a middleware that will fetch the user from the database and check whether he’s an admin or not and whether to proceed with the request or return an unauthorized response. 😊
You can find the result of this tutorial on github here
This tutorial is a natural follow-up of How to build Web Auth with Session. You can either go for that tutorial first and come back later or be a rebel, skip it and read on 😊
Index
1. Create a new project
2. Generate Xcode project
3. Adjust User Model
4. Admin Register View
5. Create a Middleware
6. Create a view only accessible for an Admin
7. Where to go from here
1. Create a new project
We will use the outcome of the aforementioned tutorial as a template to create our new project:
vapor new projectName --template=vaporberlin/web-auth-with-session
2. Generate Xcode project
Before we generate an Xcode project we would have to change the package name within Package.swift and remove one dependency that we wont need:
// swift-tools-version:4.0
import PackageDescriptionlet package = Package(
name: "projectName", // changed
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0")
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite"]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"]),
]
)
Now in the terminal at the root directory projectName/ execute:
vapor update -y
It may take a bit fetching the dependency, but when done you should have a project structure like this:
projectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Controllers/
│ │ │ └── UserController.swift
│ │ ├── Models/
│ │ │ └── User.swift
│ │ ├── app.swift
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run/
│ └── main.swift
├── Tests/
├── Resources/
│ └── Views/
│ └── userview.leaf
├── Public/
├── Dependencies/
└── Products/
3. Adjust User Model
We will add an isAdmin flag to our user model in order to be able to decide whether a user is allowed to do certain things. So in Models/User.swift add:
import FluentSQLite
import Vapor
import Authenticationfinal class User: SQLiteModel {
var id: Int?
var email: String
var password: String
var isAdmin: Bool?init(
id: Int? = nil,
email: String,
password: String,
isAdmin: Bool? = nil
) {
self.id = id
self.email = email
self.password = password
self.isAdmin = isAdmin
}
}extension User: Content {}
extension User: Migration {}extension User: PasswordAuthenticatable {
...
}extension User: SessionAuthenticatable {}
We defined isAdmin optional so we don’t have to provide a value for it when registering a normal user through the current login view.
4. Admin Register View
Let’s create a new view and a new register-route specifically for the admin. To do that we can simply copy the already existing register.leaf and re-name the new file to admin-register.leaf. It should look like this:
<!DOCTYPE html>
<html>
<head>
<title>Middleware</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head> <body class="container">
<br />
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h3 class="card-title">Admin Register</h3>
<form action="/admin-register" method="POST">
<div class="form-group">
<label for="email">Email</label>
<input type="email" name="email" class="form-control" id="email" />
</div> <div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" class="form-control" id="password" />
</div> <div class="form-group">
<input type="submit" class="btn btn-block btn-primary" value="register" />
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
Here we only adjusted the action of the form so we POST the form to our new endpoint that we will create in 3.. 2.. 1.. in routes.swift add:
import Vapor
import Authenticationpublic func routes(_ router: Router) throws { let userController = UserController()
router.get("register", use: userController.renderRegister)
router.post("register", use: userController.register)
router.get("login", use: userController.renderLogin) let authSessionRouter = router.grouped(User.authSessionsMiddleware())
authSessionRouter.post("login", use: userController.login) let protectedRouter = authSessionRouter.grouped(RedirectMiddleware<User>(path: "/login"))
protectedRouter.get("profile", use: userController.renderProfile) router.get("logout", use: userController.logout) /// Admin
router.get("admin-register", use: userController.renderAdminRegister)
router.post("admin-register", use: userController.adminRegister)
}
And now we go into UserController.swift and implement both routes 😊
import Vapor
import FluentSQL
import Cryptofinal class UserController { func renderRegister(_ req: Request) throws -> Future<View> {
...
} func register(_ req: Request) throws -> Future<Response> {
...
} func renderLogin(_ req: Request) throws -> Future<View> {
...
} func login(_ req: Request) throws -> Future<Response> {
...
} func renderProfile(_ req: Request) throws -> Future<View> {
...
} func logout(_ req: Request) throws -> Future<Response> {
...
} /// MARK: Admin func renderAdminRegister(_ req: Request) throws -> Future<View> {
return try req.view().render("admin-register")
} func adminRegister(_ req: Request) throws -> Future<Response> {
return try req.content.decode(User.self).flatMap { user in
return User.query(on: req).filter(\User.email == user.email).first().flatMap { result in
if let _ = result {
return Future.map(on: req) {
return req.redirect(to: "/admin-register")
}
} user.password = try BCryptDigest().hash(user.password)
user.isAdmin = true return user.save(on: req).map { _ in
return req.redirect(to: "/login")
}
}
}
}
}
If you look closely you will notice that the adminRegister-function doesn’t differ from the normal register-function all that much except for setting the isAdmin property to true before saving the user. We are all prepared now to create a view or a functionality that only an admin is allowed to access 🗝✨
5. Create a Middleware
Let’s have a look at how every Middleware would look like at their minimum:
struct MyCoolMiddleware: Middleware {
func respond(
to request: Request,
chainingTo next: Responder
) throws -> Future<Response> {
...
}
}
Yes it is actually that simple hahah it’s one single function. So small yet so powerful. You will have access to the request coming in and will be able to decide whether you want the request to go further (next) or even abort.
Okay no let’s create a new group under Sources/App/ with a right click on App/ and selecting New Group and inside there create a new swift file, name it AdminMiddleware and put in the following:
import Vaporstruct AdminMiddleware: Middleware { func respond(
to request: Request,
chainingTo next: Responder
) throws -> Future<Response> {
let user = try request.authenticated(User.self) if user?.isAdmin == nil {
throw Abort(.unauthorized)
} return try next.respond(to: request)
}
}
What’s happening here you ask? Well Watson, let me explain. We are defining a struct and conform to Middleware. Doing that enables us to initiate and pass our Middleware into the grouped function of our Router in routes.swift which we’ll do in a minute. That way we can precisely define which routes should have our custom AdminMiddleware running in front 👌🏻.
Next we have our respond(…) function that is defined in the Middleware protocol. It receives two arguments one being the Request and the other being a Responder. The request is what we want to work with right? We want to make decision based on what request came in. The Responder simply enables us to let the request proceed to the next station it would normally go.
6. Create a view only accessible for an Admin
Let’s create a new view and a new route restricted to be only accessible by the admin. Within Resources/Views/ create a new file named user-list.leaf and insert the following:
<!DOCTYPE html>
<html>
<head>
<title>Middleware</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body class="container">
<br />
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body"> <h3 class="panel-title">User List</h3> <ul class="list-group">
#for(user in userlist) {
<li class="list-group-item">
#(user.email)
<span class="badge badge-info">
#if(user.isAdmin) { admin } else { user }
</span>
</li>
}
</ul> </div>
</div>
</div>
</div> </body>
</html>
We are simply looping through all users and display his email and his role. Now to the controller function - In Controllers/UserController.swift add:
import Vapor
import FluentSQL
import Cryptofinal class UserController { func renderRegister(_ req: Request) throws -> Future<View> {
...
} func register(_ req: Request) throws -> Future<Response> {
...
} func renderLogin(_ req: Request) throws -> Future<View> {
...
} func login(_ req: Request) throws -> Future<Response> {
...
} func renderProfile(_ req: Request) throws -> Future<View> {
...
} func logout(_ req: Request) throws -> Future<Response> {
...
} /// MARK: Admin func renderAdminRegister(_ req: Request) throws -> Future<View> {
...
} func adminRegister(_ req: Request) throws -> Future<Response> {
...
} func userList(_ req: Request) throws -> Future<View> {
return User.query(on: request).all().flatMap { userList in
return try req.view().render("user-list", ["userlist": userList])
}
}
}
Last Step: Add a new route in routes.swift and secure it with our Middleware:
import Vapor
import Authenticationpublic func routes(_ router: Router) throws { let userController = UserController()
router.get("register", use: userController.renderRegister)
router.post("register", use: userController.register)
router.get("login", use: userController.renderLogin) let authSessionRouter = router.grouped(User.authSessionsMiddleware())
authSessionRouter.post("login", use: userController.login) let protectedRouter = authSessionRouter.grouped(RedirectMiddleware<User>(path: "/login"))
protectedRouter.get("profile", use: userController.renderProfile) router.get("logout", use: userController.logout) /// Admin
router.get("admin-register", use: userController.renderAdminRegister)
router.post("admin-register", use: userController.adminRegister)
let adminRouter = protectedRouter.grouped(AdminMiddleware())
adminRouter.get("users", use: userController.userList)
}
We are using the protectedRouter to group with the AdminMiddleware to a new adminRouter so that only logged in user that are also admin can access the routes defined by that router 🙌🏻
Are you ready to test it? Me too! Okay here is how you should test if it works:
• register a normal user
http://localhost:8080/register• login as that normal user
http://localhost:8080/login• try to access the list of users
http://localhost:8080/users (should return unauthorized)• logout (on your profile)
http://localhost:8080/profile• register an admin user
http://localhost:8080/admin-register• login as that admin user
http://localhost:8080/login• try to access the list of users
http://localhost:8080/users (it should allow you this!)
That’s it! You successfully implemented your first Middleware 🙌🏻 🎉🔥✨
7. Where to go from here
You can find a list of all tutorials with example projects on Github here:
👉🏻 https://github.com/vaporberlin/vaporschool