Tutorial: How to build Basic 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 basic 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 and generate a new project
2. Adjust Model: User
3. Create View: Register
4. Adjust UserController: Add Register-Routes
5. Create View: Login
6. Adjust UserController: Add Login-Routes
7. Create View: Profile
8. Adjust UserController: Add Profile-Route
9. Adjust UserController: Add Logout-Route
10. BONUS


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 --branch=vapor-2

Before we generate an Xcode project we would have to add the Auth-Provider as a dependency and also change the package name within Package.swift:

// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "projectName", // changed
products: [
.library(name: "App", targets: ["App"]),
.executable(name: "Run", targets: ["Run"])
],
  dependencies: [
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
.package(url: "https://github.com/vapor/leaf-provider.git", .upToNextMajor(from: "1.1.0")),
.package(url: "https://github.com/vapor/fluent-provider.git", .upToNextMajor(from: "1.3.0")),
.package(url: "https://github.com/vapor/auth-provider.git", .upToNextMajor(from: "1.2.0")) // added
],
targets: [
.target(name: "App", dependencies: ["Vapor", "LeafProvider", "FluentProvider", "AuthProvider"],
exclude: [
"Config",
"Public",
"Resources",
]
),
    ...
]
)

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
│ │ ├── Routes/
│ │ │ └── Routes.swift
│ │ └── Setup/
│ │ ├── Config+Setup.swift
│ │ └── Droplet+Setup.swift
│ └── Run/
├── Tests/
├── Config/
├── Resources/
├── Public/
├── Dependencies/
└── Products/

2. Adjust Model: User

In our Models/User.swift we extend our User by PasswordAuthenticatable and SessionPersistable, change all username occurrences to email and add another property password:

import Vapor
import FluentProvider
import AuthProvider // added
final class User: Model {
  var storage = Storage()
var email: String
var password: String
  init(email: String, password: String) {
self.email = email
self.password = password
}
  func makeRow() throws -> Row {
var row = Row()
try row.set("email", email)
try row.set("password", password)
return row
}
  init(row: Row) throws {
self.email = try row.get("email")
self.password = try row.get("password")
}
}
// MARK: Fluent Preparation
extension User: Preparation {
  static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string("email")
builder.string("password")
}
}
  static func revert(_ database: Database) throws {
try database.delete(self)
}
}
// MARK: Node
extension User: NodeRepresentable {
  func makeNode(in context: Context?) throws -> Node {
var node = Node(context)
try node.set("id", id)
try node.set("email", email)
return node
}
}
extension User: PasswordAuthenticatable {}
extension User: SessionPersistable {}

3. 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>Basic Auth</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
  <body class="container">
<br />
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
        <div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">Register</h3>
</div>
<div class="panel-body">

<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: Remember that the names in our input-tags are the keys we will use in our controller function in order to access the values of the input fields 🤓

4. 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:

final class UserController {
let drop: Droplet
  init(drop: Droplet) {
self.drop = drop
}
  // no functions in here anymore
}

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

final class UserController {
let drop: Droplet
  init(drop: Droplet) {
self.drop = drop
}
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
return try drop.view.make("register")
}

}

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

import Vapor
extension Droplet {
  func setupRoutes() throws {
    let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
}
}

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:

final class UserController {
...
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postRegister(_ req: Request) throws -> ResponseRepresentable {
    guard
let email = req.formURLEncoded?["email"]?.string,
let password = req.formURLEncoded?["password"]?.string
else {
return "either email or password is missing"
}
    guard
try User.makeQuery().filter("email", email).first() == nil
else {
return "email already exists"
}
    let user = User(email: email, password: password)
try user.save()
    return Response(redirect: "/login")
}

}

And finally in our routes within Routes/Routes.swift add:

import Vapor
extension Droplet {
  func setupRoutes() throws {
    let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
}
}

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 give you “email already exists” now! Means everything works just perfectly 🙌🏻

5. 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>Basic Auth</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
  <body class="container">
<br />
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
        <div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">Login</h3>
</div>
<div class="panel-body">

<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 login.leaf and go to Editor>Syntax Coloring>HTML 😉

6. Adjust UserController: Add Login-Routes

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

final class UserController {
...
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postRegister(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getLoginView(_ req: Request) throws -> ResponseRepresentable {
return try drop.view.make("login")
}

}

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

import Vapor
extension Droplet {
  func setupRoutes() throws {
    let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("login", handler: userController.getLoginView)
}
}

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 authenticate the user and persist him in a session. So first in Routes/Routes.swift add:

import Vapor
import AuthProvider
import Sessions
extension Droplet {
  func setupRoutes() throws {
    let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("login", handler: userController.getLoginView)
    let persistMW = PersistMiddleware(User.self)
let memory = MemorySessions()
let sessionMW = SessionsMiddleware(memory)
let loginRoute = grouped([sessionMW, persistMW])
    loginRoute.post("login", handler: userController.postLogin)
}
}

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 Middlewares 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 SessionPersistable which we did in Step 2 of this tutorial. Second: initialize the PersistMiddleware with our User. Then we decide what storage we want to use to persist the user on the server which is Memory here (it also means all session data will be purged once we restart the application) and initialize our SessionsMiddleware with it. The next thing we do with loginRoute is creating a RouteBuilder that has both Middlewares so their functionality are available for all routes we create with that RouteBuilder.

That’s why we can now be certain within our yet to be implemented postLogin function we can authenticate and persist a user. Let’s do it!

Within our Controllers/UserController.swift add the following:

import AuthProvider
final class UserController {
...
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postRegister(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getLoginView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postLogin(_ req: Request) throws -> ResponseRepresentable {
    guard
let email = req.formURLEncoded?["email"]?.string,
let password = req.formURLEncoded?["password"]?.string
else {
return "either email or password is missing"
}
    let credentials = Password(username: email, password: password)
    // returns matching user (throws error if user doesn't exist)
let user = try User.authenticate(credentials)
    // persists user and creates a session cookie
req.auth.authenticate(user)
    return Response(redirect: "/profile")
}

}

Ohw I love how easy it actually is! If we would now cmd+r our project and go to /register to register ourself and then get redirected to /login and if we now type in our credentials we used for registration we will get redirected to a yet not existing route /profile BUT if you open up your developer console in your browser and look for your cookies you’ll find a vapor-session!

7. Create View: Profile

We are almost through! Next thing is to create a profile view so create a new file within Resources/Views/ and name it profile.leaf and add:

<!DOCTYPE html>
<html>
<head>
<title>Basic Auth</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
  <body class="container">
<br />
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
        <div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">Profile</h3>
</div>
<div class="panel-body">
<p>Email: #(user.email)</p>
</div>
</div>
      </div>
</div>

</body>
</html>

8. Adjust UserController: Add Profile-Route

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

import Vapor
import AuthProvider
import Sessions
extension Droplet {
  func setupRoutes() throws {
    let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("login", handler: userController.getLoginView)
    let persistMW = PersistMiddleware(User.self)
let memory = MemorySessions()
let sessionMW = SessionsMiddleware(memory)
let loginRoute = grouped([sessionMW, persistMW])
loginRoute.post("login", handler: userController.postLogin)
    let passwordMW = PasswordAuthenticationMiddleware(User.self)
let authRoute = grouped([sessionMW, persistMW, passwordMW])
authRoute.get("profile", handler: userController.getProfileView)

}
}

Again we have an awesome Middleware that does the authentication work for us. For a protected route we sure need again access to the persisted user in the session cookie and we add a Middleware that would use that data for an authentication check. That’s why we create a RouteBuilder named authRoute that has not only the SessionsMiddleware and the PersistMiddleware but also the PasswordAuthenticationMiddleware. All routes created with that RouteBuilder are secured to be accessible only if a user has logged in before.

Within our Controllers/UserController.swift return the profile view with:

import AuthProvider
final class UserController {
...
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
}
  func postRegister(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getLoginView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postLogin(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getProfileView(_ req: Request) throws -> ResponseRepresentable {
    // returns user from session
let user: User = try req.auth.assertAuthenticated()
return try drop.view.make("profile", ["user": try user.makeNode(in: nil)])
}

}

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

9. Adjust UserController: Add Logout-Route

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

import AuthProvider
final class UserController {
...
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
}
  func postRegister(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getLoginView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postLogin(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getProfileView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func logout(_ req: Request) throws -> ResponseRepresentable {
try req.auth.unauthenticate()
return Response(redirect: "/login")
}

}

And then go to Routes/Routes.swift and add:

import Vapor
import AuthProvider
import Sessions
extension Droplet {
  func setupRoutes() throws {
    let userController = UserController(drop: self)
get("register", handler: userController.getRegisterView)
post("register", handler: userController.postRegister)
get("login", handler: userController.getLoginView)
    let persistMW = PersistMiddleware(User.self)
let memory = MemorySessions()
let sessionMW = SessionsMiddleware(memory)
let loginRoute = grouped([sessionMW, persistMW])
loginRoute.post("login", handler: userController.postLogin)
loginRoute.get("logout", handler: userController.logout)
    let passwordMW = PasswordAuthenticationMiddleware(User.self)
let authRoute = grouped([sessionMW, persistMW, passwordMW])
authRoute.get("profile", handler: userController.getProfileView)
}
}
NOTE: We use the RouteBuilder loginRoute here because we need access to the user who was persisted in the session in order to delete (unauthenticate) him 😊

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

<!DOCTYPE html>
<html>
<head>
<title>Basic Auth</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<br />
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
        <div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">Profile</h3>
</div>
<div class="panel-body">
<p>Email: #(user.email)</p>
<a href="/logout" class="btn btn-block btn-warning">
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 basic auth using session 🎉 🎉 🎉

10. BONUS: Store the users password hashed instead of plain

It’s not complicated, quickly implemented and here we go: we would adjust our User within Models/User.swift to the following:

import Vapor
import FluentProvider
import AuthProvider
final class User: Model {
...
}
// MARK: Fluent Preparation
extension User: Preparation {
...
}
// MARK: Node
extension User: NodeRepresentable {
...
}
extension User: PasswordAuthenticatable {
public var hashedPassword: String? {
return password
}

public static var passwordVerifier: PasswordVerifier? {
return MyVeryOwnPasswordVerifier()
}

}
extension User: SessionPersistable {}
struct MyVeryOwnPasswordVerifier: PasswordVerifier {
func verify(password: Bytes, matches hash: Bytes) throws -> Bool {
return try BCryptHasher().verify(password: password, matches: hash)
}
}
NOTE: The code above is sorta self explaining but in case it’s not don’t hesitate to ask or comment if you need any further explanation or help 😊!

Now the only thing left is to hash the users password before we store him. So within Controllers/UserController.swift add:

import AuthProvider
final class UserController {
...
  func getRegisterView(_ req: Request) throws -> ResponseRepresentable {
}
  func postRegister(_ req: Request) throws -> ResponseRepresentable {

guard
let email = req.formURLEncoded?["email"]?.string,
let password = req.formURLEncoded?["password"]?.string
else {
return "either email or password is missing"
}
    guard
try User.makeQuery().filter("email", email).first() == nil
else {
return "email already exists"
}
    let hashedPassword = try BCryptHasher().make(password.bytes).makeString()
let user = User(email: email, password: hashedPassword)
try user.save()
    return Response(redirect: "/login")
}
  func getLoginView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func postLogin(_ req: Request) throws -> ResponseRepresentable {
...
}
  func getProfileView(_ req: Request) throws -> ResponseRepresentable {
...
}
  func logout(_ req: Request) throws -> ResponseRepresentable {
...
}
}

And. We. Are. Done. Through. Finished. Happy. Glad. Skilled. Level Up. 😊✨


Thank you really a lot for reading! If you have any suggestions or improvements let me know! I would love to hear from you! 😊