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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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)
// 1
extension User: ModelSessionAuthenticatable {}
// 2
extension User: ModelCredentialsAuthenticatable {}

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
  }
}
// 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)
}
<!-- 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
// 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)
  }
}
// 1
routes.get("login", use: loginHandler)
// 2
let credentialsAuthRoutes = 
  routes.grouped(User.credentialsAuthenticator())
// 3
credentialsAuthRoutes.post("login", use: loginPostHandler)

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.

let authSessionsRoutes = 
  routes.grouped(User.sessionAuthenticator())
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)
let protectedRoutes = authSessionsRoutes
  .grouped(User.redirectMiddleware(path: "/login"))
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)

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
let acronym = Acronym(
  short: data.short, 
  long: data.long,
  userID: data.userID)
let user = try req.auth.require(User.self)
let acronym = try Acronym(
  short: data.short, 
  long: data.long,
  userID: user.requireID())
let user = try req.auth.require(User.self)
let userID = try user.requireID()
acronym.$user.id = userID
<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>
let users: [User]
let context = CreateAcronymContext()
return req.view.render("createAcronym", context)
let users: [User]
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)
      }
  }
}

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: "/")
}
authSessionsRoutes.post("logout", use: logoutHandler)
<!-- 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
let userLoggedIn: Bool
// 1
let userLoggedIn = req.auth.has(User.self)
// 2
let context = IndexContext(
  title: "Home page", 
  acronyms: acronyms, 
  userLoggedIn: userLoggedIn)

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.

<!-- 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
<link rel="stylesheet" href="/styles/style.css">
mkdir Public/styles
touch Public/styles/style.css
#cookie-footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 60px;
  line-height: 60px;
  background-color: #f5f5f5;
}
touch Public/scripts/cookies.js
// 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;
}
let showCookieMessage: Bool
// 1
let showCookieMessage =
  req.cookies["cookies-accepted"] == nil
// 2
let context = IndexContext(
  title: "Home page",
  acronyms: acronyms,
  userLoggedIn: userLoggedIn,
  showCookieMessage: showCookieMessage)

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.

let csrfToken: String
// 1
let token = [UInt8].random(count: 16).base64
// 2
let context = CreateAcronymContext(csrfToken: token)
// 3
req.session.data["CSRF_TOKEN"] = token
#if(csrfToken):
  <input type="hidden" name="csrfToken" value="#(csrfToken)">
#endif
let csrfToken: String?
// 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)
}

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now