Tutorial: How to build Web Auth with Session

Ohw this is a small step in coding but a huge step for our skillset! At the end of this tutorial you will have a register-view, a login-view and a profile-view and you will learn how to build authentication, use session to remember a logged in user and how to secure a view so only logged in user can access it✨

You can find the result of this tutorial on github here

This tutorial is a natural follow-up of How to write Controllers. 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 Model: User
4. Create View: Register
5. Adjust UserController: Add Register-Routes
6. Create View: Login
7. Adjust UserController: Add Login-Routes
8. Create View: Profile
9. Adjust UserController: Add Profile-Route
10. Adjust UserController: Add Logout-Route
11. Where to go from here


1. Create and generate 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/my-first-controller

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 PackageDescription
let package = Package(
name: "projectName", // changed
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/auth.git", from: "2.0.0-rc"), // added
.package(url: "https://github.com/vapor/crypto.git", from: "3.0.0") // added

],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite", "Authentication", "Crypto"]), // added
.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 Model: User

Since we are going to have the Authentication library do all the work for us regarding authentication, all we have to do is let it know what the credentials for the authentication are. And so in our Models/User.swift we will extend our User by PasswordAuthenticatable and for being able to store him in a session by SessionAuthenticatable:

import FluentSQLite
import Vapor
import Authentication // added
final class User: SQLiteModel {
var id: Int?
var email: String // added
var password: String // added
  init(id: Int? = nil, email: String, password: String) {
self.id = id
self.email = email
self.password = password
}
}
extension User: Content {}
extension User: Migration {}
extension User: PasswordAuthenticatable {
static var usernameKey: WritableKeyPath<User, String> {
return \User.email
}
  static var passwordKey: WritableKeyPath<User, String> {
return \User.password
}
}
extension User: SessionAuthenticatable {}

We have removed the username property and introduced an email property. Conforming to the PasswordAuthenticatable protocol asks us to tell where to find the usernameKey and where to find the passwordKey and really a usernameKey could be an email, an actual username or even a phone number if you want your user to register with that instead 😊


4. Create View: Register

Nice! Let’s implement a view with a form that has an input for an email and one for a password! First we’ll rename our userview.leaf to register.leaf within Resources/Views/ and then adjust the content to the following:

<!DOCTYPE html>
<html>
<head>
<title>Web Auth</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">Register</h3>
<form action="/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>
NOTE: The name attribute in our input-tags are the keys that are mapped to the value of the input and which must be named the same as the property of our User class that we will use in our controller function. That way we can use codable to create an instance of that user holding these values. More in a second 🤓

5. Adjust UserController: Add Register-Routes

Next we need to implement a route that would return our register view. But first let’s remove all functions inside our Controllers/UserController.swift:

import Vapor
final class UserController {
// no code here anymore
}

Now we’re good to go to implement a route that returns our register view:

import Vapor
final class UserController {
  func renderRegister(_ req: Request) throws -> Future<View> {
return try req.view().render("register")
}

}

Let’s not forget to remove/add our routes in routes.swift:

import Vapor
public func routes(_ router: Router) throws {
  let userController = UserController()
router.get("register", use: userController.renderRegister)
}

If you now cmd+r or run everything should built without any error and we should see our nice register-form when opening /register in our browser 😊!

Note: make sure to select Run as a scheme next to your button before running the app

To finish our registration all that’s left is the route that handles our register-form. So within our Controllers/UserController.swift add:

import Vapor
import FluentSQL // added
import Crypto // added
final class UserController {
  func renderRegister(_ req: Request) throws -> Future<View> {
...
}
  func register(_ req: Request) throws -> Future<Response> {
return try req.content.decode(User.self).flatMap { user in
return try User.query(on: req).filter(\User.email == user.email).first().flatMap { result in
if let _ = result {
return Future.map(on: req) { _ in
return req.redirect(to: "/register")
}
}
        user.password = try BCryptDigest().hash(user.password)
        return user.save(on: req).map { _ in
return req.redirect(to: "/login")
}
}
}
}

}

Okay so we are using codable to create an instance of user with the data that we’re sending with the form. Next is we’re making sure the email is unique by searching for a user that has the same email. If we have a result we know that email isn’t unique so we redirect back to the register screen. Otherwise we are continuing and override the password property with the password but hashed and then attempt to save the user and then return to the login screen.

Now finally in our routes within routes.swift add:

import Vapor
public func routes(_ router: Router) throws {
  let userController = UserController()
router.get("register", use: userController.renderRegister)
router.post("register", use: userController.register)
}

You could now cmd+r to run the project, go to /register and try to register yourself with an email. It should redirect you to /login that doesn’t exist, yet. Just go back to /register and try to register with that same email again, it will redirect you back to the register view! Means everything works perfectly 🙌🏻


6. Create View: Login

Okay so in order to log in we’ll need a view with a form that has an input for an email and one for a password. So within Resources/Views/ create a new file and name it login.leaf and add:

<!DOCTYPE html>
<html>
<head>
<title>Web Auth</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">Login</h3>

<form action="/login" 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-success" value="login" />
</div>
</form>
</div>
</div>
      </div>
</div>

</body>
</html>
NOTE: Select the login.leaf file and go to Editor>Syntax Coloring>HTML 😉

7. Adjust UserController: Add Login-Routes

The first route for returning our login view is almost too simple. Add the following within our Controllers/UserController.swift:

import Vapor
import FluentSQL
import Crypto
final class UserController {
  func renderRegister(_ req: Request) throws -> Future<View> {
...
}
  func register(_ req: Request) throws -> Future<Response> {
...
}
  func renderLogin(_ req: Request) throws -> Future<View> {
return try req.view().render("login")
}

}

And then go to our routes.swift and add our new route:

import Vapor
public 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)
}

If you would now cmd+r and register yourself under /register you’ll be successfully redirected to our new shiny login view 😊!

The next route will take a little more. We need a route that would handle the login-values from our login-form. With handle I mean to receive, authenticate and persist the user in a session. So first in routes.swift add:

import Vapor
import Authentication // added
public 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)

}

Second go to our configure.swift and add the following:

import Vapor
import Leaf
import FluentSQLite
import Authentication // added
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
  ...
  try services.register(AuthenticationProvider())
  var middlewares = MiddlewareConfig.default()
middlewares.use(SessionsMiddleware.self)
services.register(middlewares)
  config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)
}

Let me explain for short what we did here. As mentioned we want to be able to persist our User within a session. It’s super nice we have a Middleware that do the work for us. In order to have persistence working we only need a few things. First: we need to conform our User to SessionAuthenticatable which we did in Step 3of this tutorial. Second: we initialize and create a Router with authSessionsMiddleware. Then we define a route that should have all the functionalities of the middleware exposed to it. In configure.swift we are registering our AuthenticationProvider, a SessionMiddleware and finally configure where to persist the session: in our memory (it also means all session data will be purged once we restart the application).

That’s why it’s safe to say within our yet to be implemented login function we can authenticate and persist a user. Let’s do it!

Within our Controllers/UserController.swift add the following:

import Vapor
import FluentSQL
import Crypto
final 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> {
return try req.content.decode(User.self).flatMap { user in
return User.authenticate(
username: user.email,
password: user.password,
using: BCryptDigest(),
on: req
).map { user in
guard let user = user else {
return req.redirect(to: "/login")
}
        try req.authenticateSession(user)
return req.redirect(to: "/profile")
}
}
}

}

Ohw I love how easy it actually is! So we are using a static function to verify that the send credentials are right. Since we hashed the password of the user with BCryptDigest in the register-function we pass it to the authentication so it is able to verify the password. If the user is not passing guard we will redirect to the login view. Otherwise we are authenticating (this is the part we are persisting the user to the session) the user to the session and redirect then to a yet not existing route /profile.

Let’s create a profile that is only accessible to a logged in user!


8. Create View: Profile

First let’s create a new file within Resources/Views/ and call it profile.leaf:

<!DOCTYPE html>
<html>
<head>
<title>Web Auth</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">Profile</h3>
<p>Email: #(user.email)</p>
</div>
</div>
</div>
</div>

</body>
</html>

9. Adjust UserController: Add Profile-Route

We will make the profile view only accessible for an authenticated user. Therefor within our routes.swift add the following:

import Vapor
import Authentication
public 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)
}

Again we have an awesome Middleware that does the authentication work for us. If you look into the RedirectMiddleware it says “Basic middleware to redirect unauthenticated requests to the supplied path”. Well that’s exactly what we want! So all it needs to know is the class we want it to check the authentication for and also pass in the route to redirect to if unauthenticated.

All routes created with that router are secured now and can only be accessed by users that have logged in before. Let’s finish up the profile view and got into our Controllers/UserController.swift and add:

import Vapor
import FluentSQL
import Crypto
final 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> {
let user = try req.requireAuthenticated(User.self)
return try req.view().render("profile", ["user": user])
}

}

What if I tell you that if you go and cmd+r to run your project now and try to access /profile you will redirect you to the login view? And that if you go and register yourself and then login that you will be redirected to /profile and then be able to access this secured view? Yes, it’s that mind-blowingly easy!


10. Adjust UserController: Add Logout-Route

Ready for a two liner? Within Controllers/UserController.swift add:

import Vapor
import FluentSQL
import Crypto
final 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> {
try req.unauthenticateSession(User.self)
return Future.map(on: req) { return req.redirect(to: "/login") }
}
}

Next go to routes.swift and add:

import Vapor
import Authentication
public 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)
}

And finally add a button to our Resources/Views/profile.leaf that fires it:

<!DOCTYPE html>
<html>
<head>
<title>Web Auth</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">Profile</h3>
<p>Email: #(user.email)</p>
<a href="/logout" class="btn btn-block btn-danger">
logout
</a>

</div>
</div>
</div>
</div>
</body>
</html>

Our final cmd+r or run and refresh of our site and that’s it! You can try to access /profile it wont work, but if you register and login yourself you can access it and from there also logout again!

That’s it! You successfully implemented web auth using session 🎉🚀✨


11. Where to go from here

You can find a list of all tutorials with example projects on Github here:
👉🏻 https://github.com/vaporberlin/vaporschool


I am really happy you read my article! If you have any suggestions or improvements of any kind let me know! I’d love to hear from you! 😊