Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

20. Web Authentication, Cookies & Sessions
Written by Tim Condon

In the previous chapters, you learned how to implement authentication in the TIL app’s API. In this chapter, you’ll see how to implement authentication for the TIL website. You’ll learn how authentication works on the web and how Vapor’s Authentication module provides all the necessary support. You’ll then see how to protect different routes on the website. Finally, you’ll learn how to use cookies and sessions to your advantage.

Web authentication

How it works

Earlier, you learned how to use HTTP basic authentication and bearer authentication to protect the API. As you’ll recall, this works by sending tokens and credentials in the request headers. However, this isn’t possible in web browsers. There’s no way to add headers to requests your browser makes with normal HTML.

To work around this, browsers and web sites use cookies. A cookie is a small bit of data your application sends to the browser to store on the user’s computer. Then, when the user makes a request to your application, the browser attaches the cookies for your site.

You combine this with sessions to authenticate users. Sessions allow you to persist state across requests. In Vapor, when you have sessions enabled, the application provides a cookie to the user with a unique ID. This ID identifies the user’s session. When the user logs in, Vapor saves the user in the session. When you need to ensure a user has logged in or to get the current authenticated user, you query the session.

Implementing sessions

Vapor manages sessions using a middleware, SessionsMiddleware. Open the project in Xcode and open configure.swift. In the middleware configuration section, add the following below app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)):

app.middleware.use(app.sessions.middleware)

This registers the sessions middleware as a global middleware for your application. It also enables sessions for all requests. Next, open User.swift and add the following at the bottom of the file:

// 1
extension User: ModelSessionAuthenticatable {}
// 2
extension User: ModelCredentialsAuthenticatable {}

Here’s what this does:

  1. Conform User to ModelSessionAuthenticatable. This allows the application to save and retrieve your user as part of a session.
  2. Conform User to ModelCredentialsAuthenticatable. This allows Vapor to authenticate users with a username and password when they log in. Since you’ve already implemented the necessary properties and function for ModelCredentialsAuthenticatable in ModelAuthenticatable, there’s nothing to do here.

Log in

To log a user in, you need two routes — one for showing the login page and one for accepting the POST request from that page. Open WebsiteController.swift and add the following at the bottom of the file to create a context for the login page:

struct LoginContext: Encodable {
  let title = "Log In"
  let loginError: Bool

  init(loginError: Bool = false) {
    self.loginError = loginError
  }
}

This provides the title of the page and a flag to indicate a login error. Next, at the bottom of WebsiteController, add a route handler for the page:

// 1
func loginHandler(_ req: Request) 
  -> EventLoopFuture<View> {
    let context: LoginContext
    // 2
    if let error = req.query[Bool.self, at: "error"], error {
      context = LoginContext(loginError: true)
    } else {
      context = LoginContext()
    }
    // 3
    return req.view.render("login", context)
}

Here’s what this does:

  1. Define a route handler for the login page that returns a future View.
  2. If the request contains the error parameter and it’s true, create a context with loginError set to true.
  3. Render the login.leaf template, passing in the context.

Create the new template, login.leaf, in Resources/Views and open the file. Replace the contents of the file with the following:

<!-- 1 -->
#extend("base"):
  #export("content"):
    <!-- 2 -->
    <h1>#(title)</h1>

    <!-- 3 -->
    #if(loginError):
      <div class="alert alert-danger" role="alert">
        User authentication error. Either your username or
        password was invalid.
      </div>
    #endif

    <!-- 4 -->
    <form method="post">
      <!-- 5 -->
      <div class="form-group">
        <label for="username">Username</label>
        <input type="text" name="username" class="form-control"
         id="username"/>
      </div>

      <!-- 6 -->
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" name="password"
         class="form-control" id="password"/>
      </div>

      <!-- 7 -->
      <button type="submit" class="btn btn-primary">
        Log In
      </button>
    </form>
  #endexport
#endextend

Here’s what’s going on in the template:

  1. Extend base.leaf and export content as required.

  2. Set the title for the page using the provided title from the context.

  3. If the context value for loginError is true, display a suitable message.

  4. Define a <form> that sends a POST request to same URL when submitted.

  5. Add an input for the user’s username. The name of the input matches the name required by ModelCredentialsAuthenticatable.

  6. Add an input for the user’s password. Note the type="password" — this tells the browser to render the input as a password field. This uses the name for password required by ModelCredentialsAuthenticatable.

  7. Add a submit button for the form.

Next, open WebsiteController.swift and, below loginHandler(_:), add the following route handler for this request:

// 1
func loginPostHandler(
  _ req: Request
) -> EventLoopFuture<Response> {
  // 2
  if req.auth.has(User.self) {
    // 3
    return req.eventLoop.future(req.redirect(to: "/"))
  } else {
    // 4
    let context = LoginContext(loginError: true)
    return req
      .view
      .render("login", context)
      .encodeResponse(for: req)
  }
}

Here’s what this does:

  1. Define a route handler that returns EventLoopFuture<Response>.
  2. Verify that the request has an authenticated User. You use middleware to perform the authentication.
  3. Redirect to the home page after the login succeeds.
  4. If the login failed, redirect back to the login page to show an error.

Finally, at the bottom of boot(routes:), register the two routes:

// 1
routes.get("login", use: loginHandler)
// 2
let credentialsAuthRoutes = 
  routes.grouped(User.credentialsAuthenticator())
// 3
credentialsAuthRoutes.post("login", use: loginPostHandler)

Here’s what this does:

  1. Route GET requests for /login to loginHandler(_:).
  2. Create a route group using ModelCredentialsAuthenticator. This middleware checks the request for the submitted form. It then verifies the credentials and authenticates the request if successful.
  3. Route POST requests for /login to loginPostHandler(_:userData:) via credentialsAuthRoutes.

Build and run the application. In your browser, visit http://localhost:8080/login. Click Log In without entering data to see the error handling.

Next, enter your credentials and click Log In again. After the app validates your credentials, it redirects you to the main acronyms list.

Protecting routes

In the API, you used GuardAuthenticationMiddleware to assert that the request contained an authenticated user. This middleware throws an authentication error if there’s no user, resulting in a 401 Unauthorized response to the client.

On the web, this isn’t the best user experience. Instead, you use RedirectMiddleware to redirect users to the login page when they try to access a protected route without logging in first. Before you can use this redirect, you must first translate the session cookie, sent by the browser, into an authenticated user.

In WebsiteController, replace the entire contents of boot(routes:), including the new routes you just added with the following:

let authSessionsRoutes = 
  routes.grouped(User.sessionAuthenticator())

This creates a route group that runs DatabaseSessionAuthenticator before the route handlers. This middleware reads the cookie from the request and looks up the session ID in the application’s session list. If the session contains a user, DatabaseSessionAuthenticator adds it to the request’s authentication cache, making the user available later in the process.

Next, register all the public routes, including the new login routes, in this route group:

authSessionsRoutes.get("login", use: loginHandler)
let credentialsAuthRoutes = 
  authSessionsRoutes.grouped(User.credentialsAuthenticator())
credentialsAuthRoutes.post("login", use: loginPostHandler)
authSessionsRoutes.get(use: indexHandler)
authSessionsRoutes.get(
  "acronyms", 
  ":acronymID",
  use: acronymHandler)
authSessionsRoutes.get("users", ":userID", use: userHandler)
authSessionsRoutes.get("users", use: allUsersHandler)
authSessionsRoutes.get("categories", use: allCategoriesHandler)
authSessionsRoutes.get(
  "categories", 
  ":categoryID",
  use: categoryHandler)

This makes the User available to these pages, even though it’s not required. This is useful for displaying user-specific content, such as a profile link, on any page you desire. Underneath these routes, add the following:

let protectedRoutes = authSessionsRoutes
  .grouped(User.redirectMiddleware(path: "/login"))

This creates a new route group, extending from authSessionsRoutes, that includes RedirectMiddleware for User. The application runs a request through RedirectMiddleware before it reaches the route handler, but after DatabaseSessionAuthenticator. This allows RedirectMiddleware to check for an authenticated user. RedirectMiddleware requires you to specify the path for redirecting unauthenticated users.

Finally, register the routes that require protection — creating, editing and deleting acronyms — to this route group:

protectedRoutes.get(
  "acronyms", 
  "create", 
  use: createAcronymHandler)
protectedRoutes.post(
  "acronyms", 
  "create",
  use: createAcronymPostHandler)
protectedRoutes.get(
  "acronyms", 
  ":acronymID", 
  "edit",
  use: editAcronymHandler)
protectedRoutes.post(
  "acronyms", 
  ":acronymID", 
  "edit",
  use: editAcronymPostHandler)
protectedRoutes.post(
  "acronyms", 
  ":acronymID", 
  "delete",
  use: deleteAcronymHandler)

Remember this includes both the GET requests and the POST requests. Build and run, then visit http://localhost:8080 in your browser.

Click Create An Acronym in the navigation bar and, this time, the app redirects you to the login page:

Enter the credentials for the seeded admin user and click Log In. The application redirects you to the main acronym list. If you click Create An Acronym again, the application lets you access the page.

Updating the site

Just like the API, now that users must login, the application knows which user is creating or editing an acronym. Still in WebsiteController.swift, find CreateAcronymFormData and remove the user ID:

let userID: UUID

This is no longer required since you can get it from the authenticated user. Next, find createAcronymPostHandler(_:data:) and replace:

let acronym = Acronym(
  short: data.short, 
  long: data.long,
  userID: data.userID)

With the following:

let user = try req.auth.require(User.self)
let acronym = try Acronym(
  short: data.short, 
  long: data.long,
  userID: user.requireID())

This gets the user from the request using require(_:), as in the API. Next, in editAcronymPostHandler(_:), add the following at the top of the method:

let user = try req.auth.require(User.self)
let userID = try user.requireID()

Again, this gets the authenticated user from the request and then gets the associated ID. It’s useful to do it here as you can throw errors in the main body of editAcronymPostHandler(_:). Finally, replace acronym.$user.id = updateData.userID with the following:

acronym.$user.id = userID

This uses the authenticated user’s ID for the updated acronym. Now, both creating and editing acronyms use the authenticated user. As a result, you no longer need to show the users in the form. Open createAcronym.leaf and remove the following code:

<div class="form-group">
  <label for="userID">User</label>
  <select name="userID" class="form-control" id="userID">
    #for(user in users):
      <option value="#(user.id)" 
        #if(editing): 
          #if(acronym.user.id == user.id): selected #endif 
        #endif>
        #(user.name)
      </option>
    #endfor
  </select>
</div>

As you use the same template for creating and editing acronyms, you only need to remove this from one place! Next, open WebsiteController.swift and remove the following from CreateAcronymContext:

let users: [User]

This is no longer required as the template doesn’t use users any longer. In createAcronymHandler(_:), address the change by replacing the body of the method with:

let context = CreateAcronymContext()
return req.view.render("createAcronym", context)

Next, remove the following from EditAcronymContext:

let users: [User]

Next, replace editAcronymHandler(_:), with the following:

func editAcronymHandler(_ req: Request) 
  -> EventLoopFuture<View> {
  return Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      acronym.$categories.get(on: req.db)
        .flatMap { categories in
          let context = EditAcronymContext(
            acronym: acronym, 
            categories: categories)
          return req.view.render("createAcronym", context)
      }
  }
}

This removes the query to get all the users and the resulting extra future. Build and run, then visit http://localhost:8080/ in your browser. Click Create An Acronym and log in again.

Note: You need to log in again after restarting because the application keeps sessions in memory. For production applications, you can use Redis or a database to persist this information and share it across server instances.

Head back to Create An Acronym and the form no longer includes the list of users:

Create an acronym. When the application redirects you to the acronym’s page, you’ll see Vapor has used the authenticated user as the acronym’s user:

Log out

When you allow users to log in to your site, you should also allow them to log out. Still in WebsiteController.swift, add the following after loginPostHandler(_:):

// 1
func logoutHandler(_ req: Request) -> Response {
  // 2
  req.auth.logout(User.self)
  // 3
  return req.redirect(to: "/")
}

Here’s what this does:

  1. Define a route handler that simply returns Response. There’s no asynchronous work in this method, so it doesn’t need to return a future.
  2. Call logout(_:) on the request. This deletes the user from the session so it can’t be used to authenticate future requests.
  3. Return a redirect to the index page.

Register the route inside boot(routes:) after credentialsAuthRoutes.post("login", use: loginPostHandler):

authSessionsRoutes.post("logout", use: logoutHandler)

This connects POST requests for /logout to logoutHandler(). You should always use POST requests for anything that changes application state. Modern browsers prefetch GET requests which could result in your users being unexpectedly logged out if you don’t use POST!

Open base.leaf and after </ul> in the navigation bar add the following:

<!-- 1 -->
#if(userLoggedIn):
  <!-- 2 -->
  <form class="form-inline" action="/logout" method="POST">
    <!-- 3 -->
    <input class="nav-link btn btn-secondary mr-sm-2" 
     type="submit" value="Log out">
  </form>
#endif

Here’s what this does:

  1. Check to see if userLoggedIn is set so you only display the logout option when a user’s logged in.
  2. Create a new form that sends a POST request to /logout.
  3. Add a submit button to the form with the value Log out and style it like a button and align it to the right.

Save the file. Next, open WebsiteController.swift and, at the bottom of IndexContext, add the following:

let userLoggedIn: Bool

This is the flag you set to tell the template the request contains a logged in user. Finally, in indexHandler(_:), replace let context = IndexContext(title: "Home page", acronyms: acronyms) with the following:

// 1
let userLoggedIn = req.auth.has(User.self)
// 2
let context = IndexContext(
  title: "Home page", 
  acronyms: acronyms, 
  userLoggedIn: userLoggedIn)

Here’s what this does:

  1. Check if the request contains an authenticated user.
  2. Pass the result to the new flag in IndexContext.

Build and run, then head to your browser. Click Create An Acronym and log in. When the application redirects you to the home page, you’ll see a new Log out option in the top right:

If you click this, then click Create An Acronym again, you’ll need to sign in as the application has logged you out.

Cookies

Cookies are widely used on the web. Everyone’s seen the cookie consent messages that pop up on a site when you first visit. You’ve already used cookies to implement authentication, but sometimes you want to set and read cookies manually.

A common way to handle the cookie consent message is to add a cookie when a user has accepted the notice (the irony!).

Open base.leaf and, above the script tag for jQuery, add the following:

<!-- 1 -->
#if(showCookieMessage):
  <!-- 2 -->
  <footer id="cookie-footer">
    <div id="cookieMessage" class="container">
      <span class="muted">
        <!-- 3 -->
        This site uses cookies! To accept this, click
        <a href="#" onclick="cookiesConfirmed()">OK</a>
      </span>
    </div>
  </footer>
  <!-- 4 -->
  <script src="/scripts/cookies.js"></script>
#endif

Here’s what the code does:

  1. Check whether a showCookieMessage flag is set for the template.
  2. If so, add a <footer> for the cookie message, styled using Bootstrap.
  3. Add an OK link for users to click. This calls cookiesConfirmed(), a JavaScript function that dismisses the cookie message.
  4. Add the JavaScript file for cookies.

Next, in base.leaf above <title>#(title) | Acronyms</title>, add the following:

<link rel="stylesheet" href="/styles/style.css">

This includes a new stylesheet for the website. You’ll use this to add custom styling to your site. Save the file.

To create this stylesheet, enter the following in Terminal:

mkdir Public/styles
touch Public/styles/style.css

Next, open style.css and add the following:

#cookie-footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 60px;
  line-height: 60px;
  background-color: #f5f5f5;
}

This styling pins the cookie message to the bottom of the page. Save the stylesheet. Next, enter the following into Terminal to create a new file in Public/scripts called cookies.js :

touch Public/scripts/cookies.js

Next, open cookies.js and add the following:

// 1
function cookiesConfirmed() {
  // 2
  $('#cookie-footer').hide();
  // 3
  var d = new Date();
  d.setTime(d.getTime() + (365*24*60*60*1000));
  var expires = "expires="+ d.toUTCString();
  // 4
  document.cookie = "cookies-accepted=true;" + expires;
}

Here’s what the JavaScript does:

  1. Define a function, cookiesConfirmed(), that the browser calls when the user clicks the OK link in the cookie message.
  2. Hide the cookie message.
  3. Create a date that’s one year in the future. Then, create the expires string required for the cookie. By default, cookies are valid for the browser session — when the user closes the browser window or tab, the browser deletes the cookie. Adding the date ensures the browser persists the cookie for a year.
  4. Add a cookie called cookies-accepted to the page using JavaScript. You’ll check to see if this cookie exists when working out whether to show the cookie consent message.

Save the file. Open WebsiteController.swift in Xcode and add the following to the bottom of IndexContext:

let showCookieMessage: Bool

This flag indicates to the template whether it should display the cookie consent message. In indexHandler(_:), replace let context = IndexContext... with the following:

// 1
let showCookieMessage =
  req.cookies["cookies-accepted"] == nil
// 2
let context = IndexContext(
  title: "Home page",
  acronyms: acronyms,
  userLoggedIn: userLoggedIn,
  showCookieMessage: showCookieMessage)

Here’s what this does:

  1. See if a cookie called cookies-accepted exists. If it doesn’t, set the showCookieMessage flag to true. You can read cookies from the request and set them on a response.
  2. Pass the flag to IndexContext so the template knows whether to show the message.

Build and run, then go to http://localhost:8080 in your browser. The site shows the cookie consent message on the page:

Click OK in the cookie consent message and your JavaScript code hides it. Refresh the page and the site won’t show the message again.

Sessions

In addition to using cookies for web authentication, you’ve also made use of sessions. Sessions are useful in a number of scenarios, including authentication.

Another such scenario is Cross-Site Request Forgery (CSRF) prevention. CSRF is where an attacker tricks a user into sending an unexpected or unintended POST request, such as a request to a bank to transfer money. If the user is logged in, the site processes the request without any issue.

The same is possible with creating acronyms in the TIL website. If someone tricked an already-authenticated user into sending a POST request to /acronyms/create, the application would create the acronym!

A common approach to solving this problem involves including a CSRF token in the form. When the application receives the POST request, it verifies that the CSRF token matches the one issued to the form. If the tokens match, the application processes the request; otherwise, it rejects the request.

To add CSRF token support, open WebsiteController.swift and add the following to the bottom of CreateAcronymContext:

let csrfToken: String

This is the CSRF token you’ll pass into the template. In createAcronymHandler(_:), replace let context = CreateAcronymContext() with the following:

// 1
let token = [UInt8].random(count: 16).base64
// 2
let context = CreateAcronymContext(csrfToken: token)
// 3
req.session.data["CSRF_TOKEN"] = token

Here’s what the new code does:

  1. Create a token using 16 bytes of randomly generated data, Base64 encoded.
  2. Initialize a CreateAcronymContext with the created token.
  3. Save the token into the request’s session data under the CSRF_TOKEN key.

Vapor persists the token in the session across different requests. When the user makes a new request and provides the cookie that identifies the session, all the session data is available. Open createAcronym.leaf and, underneath <form method="post">, add the following:

#if(csrfToken):
  <input type="hidden" name="csrfToken" value="#(csrfToken)">
#endif

This checks to see if the context contains a token. If so, the template adds a new input element to the form with the token as the value. Since this element is hidden, the browser doesn’t display the token to the user.

Save the file. Back in WebsiteController.swift, add the following to the bottom of CreateAcronymFormData:

let csrfToken: String?

This is the CSRF token that the form sends using the hidden input. The token is optional as it’s not required by the edit acronym page for now. Finally, in createAcronymPostHandler(_:data:) after let user = try req.auth.require(User.self), add the following:

// 1
let expectedToken = req.session.data["CSRF_TOKEN"]
// 2
req.session.data["CSRF_TOKEN"] = nil
// 3
guard 
  let csrfToken = data.csrfToken,
  expectedToken == csrfToken 
else {
  throw Abort(.badRequest)
}

Here’s what this does:

  1. Get the expected token from the request’s session data. This is the token you saved in createAcronymHandler(_:).
  2. Clear the CSRF token now that you’ve used it. You generate a new token with each form.
  3. Ensure the provided token is not nil and matches the expected token; otherwise, throw a 400 Bad Request error.

Build and run, then visit http://localhost:8080 in your browser. Go to the Create An Acronym page once you’ve logged in and create a new acronym. The application creates the acronym as the form provided the correct CSRF token. If you send a request without the token, either by removing it from your page or using RESTed, you’ll get a 400 Bad Request response.

Where to go from here?

In this chapter, you learned how to add authentication to the application’s web site. You also learned how to make use of both sessions and cookies. You might want to look at adding CSRF tokens to the other POST routes, such as deleting and editing acronyms. In the next chapter, you’ll learn how to use Vapor’s validation library to automatically validate objects, request data and inputs.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.