Home iOS & Swift Books Server-Side Swift with Vapor

20
Web Authentication, Cookies & Sessions Written by Tim Condon

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

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 see 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 middlewares.use(ErrorMiddleware.self):

middlewares.use(SessionsMiddleware.self)
config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)
// 1
extension User: PasswordAuthenticatable {}
// 2
extension User: SessionAuthenticatable {}

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) throws -> Future<View> {
  let context: LoginContext
  // 2
  if req.query[Bool.self, at: "error"] != nil {
    context = LoginContext(loginError: true)
  } else {
    context = LoginContext()
  }
  // 3
  return try req.view().render("login", context)
}
#// 1
#set("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>
  }

  #// 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>
}

#embed("base")
struct LoginPostData: Content {
  let username: String
  let password: String
}
import Authentication
// 1
func loginPostHandler(
  _ req: Request,
  userData: LoginPostData
) throws -> Future<Response> {
    // 2
    return User.authenticate(
      username: userData.username,
      password: userData.password,
      using: BCryptDigest(),
      on: req).map(to: Response.self) { user in
        // 3
        guard let user = user else {
          return req.redirect(to: "/login?error")
        }
        // 4
        try req.authenticateSession(user)
        // 5
        return req.redirect(to: "/")
    }
}
// 1
router.get("login", use: loginHandler)
// 2
router.post(LoginPostData.self, at: "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 authSessionRoutes =
  router.grouped(User.authSessionsMiddleware())
authSessionRoutes.get(use: indexHandler)
authSessionRoutes.get("acronyms", Acronym.parameter,
                      use: acronymHandler)
authSessionRoutes.get("users", User.parameter, use: userHandler)
authSessionRoutes.get("users", use: allUsersHandler)
authSessionRoutes.get("categories", use: allCategoriesHandler)
authSessionRoutes.get("categories", Category.parameter,
                      use: categoryHandler)
authSessionRoutes.get("login", use: loginHandler)
authSessionRoutes.post(LoginPostData.self, at: "login",
                       use: loginPostHandler)
let protectedRoutes = authSessionRoutes
  .grouped(RedirectMiddleware<User>(path: "/login"))
protectedRoutes.get("acronyms", "create",
                    use: createAcronymHandler)
protectedRoutes.post(CreateAcronymData.self, at: "acronyms",
                     "create", use: createAcronymPostHandler)
protectedRoutes.get("acronyms", Acronym.parameter, "edit",
                    use: editAcronymHandler)
protectedRoutes.post("acronyms", Acronym.parameter, "edit",
                     use: editAcronymPostHandler)
protectedRoutes.post("acronyms", Acronym.parameter, "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 CreateAcronymData and remove the user ID:

let userID: User.ID
let acronym = Acronym(short: data.short, long: data.long,
                      userID: data.userID)
let user = try req.requireAuthenticated(User.self)
let acronym = try Acronym(
  short: data.short,
  long: data.long,
  userID: user.requireID())
let user = try req.requireAuthenticated(User.self)
acronym.userID = try user.requireID()
<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.userID == user.id){selected}}>
      #(user.name)
    </option>
    }
  </select>
</div>
let users: Future<[User]>
let context = CreateAcronymContext(
  users: User.query(on: req).all())
let context = CreateAcronymContext()
let users: Future<[User]>
let context = EditAcronymContext(
  acronym: acronym,
  users: users,
  categories: categories)
let context = EditAcronymContext(
  acronym: acronym,
  categories: categories)
let users = User.query(on: req).all()

Log out

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

// 1
func logoutHandler(_ req: Request) throws -> Response {
  // 2
  try req.unauthenticateSession(User.self)
  // 3
  return req.redirect(to: "/")
}
authSessionRoutes.post("logout", use: logoutHandler)
#// 1
#if(userLoggedIn) {
  #// 2
  <form class="form-inline" action="/logout" method="POST">
    #// 3
    <input class="nav-link btn" type="submit"
     value="Log out">
  </form>
}
let userLoggedIn: Bool
// 1
let userLoggedIn = try req.isAuthenticated(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>
}
<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.http.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 = try CryptoRandom()
  .generateData(count: 16)
  .base64EncodedString()
// 2
let context = CreateAcronymContext(csrfToken: token)
// 3
try req.session()["CSRF_TOKEN"] = token
#if(csrfToken) {
  <input type="hidden" name="csrfToken" value="#(csrfToken)">
}
let csrfToken: String?
// 1
let expectedToken = try req.session()["CSRF_TOKEN"]
// 2
try req.session()["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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.