Home iOS & Swift Books Server-Side Swift with Vapor

19
API Authentication, Part 2 Written by Tim Condon

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

Now that you’ve implemented API authentication, neither your tests nor the iOS application work any longer. In this chapter, you’ll learn the techniques needed to account for the new authentication requirements.

Note: You must have PostgreSQL set up and configured in your project. If you still need to do this, follow the steps in Chapter 6, “Configuring a Database”.

Updating the tests

Now you’ve protected all the routes in your API, you need to update the tests. In Xcode, set the scheme to TILApp-Package and the deployment target to My Mac. Open UserTests.swift, find testUserCanBeSavedWithAPI() and replace:

let user = User(name: usersName, username: usersUsername)

With the following:

let user = User(
  name: usersName,
  username: usersUsername,
  password: "password")

This includes the password so the JSON body in the request is set properly. Next, open Models+Testable.swift and under import FluentPostgreSQL add the following:

import Crypto

This imports the Crypto module to allow you to use BCrypt. Next, replace create(name:username:on:) in the User extension with the following:

// 1
static func create(
  name: String = "Luke",
  username: String? = nil,
  on connection: PostgreSQLConnection
) throws -> User {
  let createUsername: String
  // 2
  if let suppliedUsername = username {
    createUsername = suppliedUsername
  // 3
  } else {
    createUsername = UUID().uuidString
  }

  // 4
  let password = try BCrypt.hash("password")
  let user = User(
    name: name,
    username: createUsername,
    password: password)
  return try user.save(on: connection).wait()
}

Here’s what you changed:

  1. Make the username parameter an optional string that defaults to nil.
  2. If a username is supplied, use it.
  3. If a username isn’t supplied, create a new, random one using UUID. This ensures the username is unique as required by the migration.
  4. Create a user.

In Terminal, run the following:

# 1
docker stop postgres-test
# 2
docker rm postgres-test
# 3
docker run --name postgres-test -e POSTGRES_DB=vapor-test \
  -e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=password \
  -p 5433:5432 -d postgres

Here’s what this does:

  1. Stop the test PostgreSQL container.
  2. Remove the test PostgreSQL container; this removes the existing database.
  3. Run the test container again as described in Chapter 11, “Testing”.

If you run the tests now, they crash since calls to any authenticated routes fail. You need to provide authentication for these requests.

Open Application+Testable.swift and replace

import App

with the following:

@testable import App
import Authentication

This enables you to use Token and imports the authentication module. Next, replace the signature of sendRequest<T>(to:method:headers:body:) with the following:

func sendRequest<T>(
  to path: String,
  method: HTTPMethod,
  headers: HTTPHeaders = .init(),
  body: T? = nil,
  loggedInRequest: Bool = false,
  loggedInUser: User? = nil
) throws -> Response where T: Content {

This adds loggedInRequest and loggedInUser as parameters. You use these to tell your tests to send an Authorization header or use a specified user, as required.

Next, before let responder = try self.make(Responder.self) add the following:

var headers = headers
// 1
if (loggedInRequest || loggedInUser != nil) {
  let username: String
  // 2
  if let user = loggedInUser {
    username = user.username
  } else {
    username = "admin"
  }
  // 3
  let credentials = BasicAuthorization(
    username: username,
    password: "password")

  // 4
  var tokenHeaders = HTTPHeaders()
  tokenHeaders.basicAuthorization = credentials

  // 5
  let tokenResponse = try self.sendRequest(
    to: "/api/users/login",
    method: .POST,
    headers: tokenHeaders)
  // 6
  let token = try tokenResponse.content.syncDecode(Token.self)
  // 7
  headers.add(name: .authorization,
              value: "Bearer \(token.token)")
}

Here’s what the new code does:

  1. Determine if this request requires authentication.

  2. If a user is supplied, create a BasicAuthorization type using the user’s details. Note: This requires you to know the user’s password. As all the users in your tests have the password “password”, this isn’t an issue. If no user is specified, use “admin”.

  3. Create a BasicAuthorization credential.

  4. Add the basic authorization header for the login request.

  5. Send a request to log in the user and get the response.

  6. Decode the Token from the login request.

  7. Add the token to the authorization header for the request you’re trying to send.

Change the remaining four request helpers in Application+Testable.swift to accept loggedInRequest and loggedInUser parameters and pass them to sendRequest<T>(to:method:headers:body:loggedInRequest:loggedInUser:):

func sendRequest(
  to path: String,
  method: HTTPMethod,
  headers: HTTPHeaders = .init(),
  loggedInRequest: Bool = false,
  loggedInUser: User? = nil
) throws -> Response {
  let emptyContent: EmptyContent? = nil
  return try sendRequest(
    to: path, method: method,
    headers: headers, body: emptyContent,
    loggedInRequest: loggedInRequest,
    loggedInUser: loggedInUser)
}

func sendRequest<T>(
  to path: String,
  method: HTTPMethod,
  headers: HTTPHeaders,
  data: T,
  loggedInRequest: Bool = false,
  loggedInUser: User? = nil
) throws where T: Content {
  _ = try self.sendRequest(
    to: path, method: method,
    headers: headers, body: data,
    loggedInRequest: loggedInRequest,
    loggedInUser: loggedInUser)
}

func getResponse<C, T>(
  to path: String,
  method: HTTPMethod = .GET,
  headers: HTTPHeaders = .init(),
  data: C? = nil, decodeTo type: T.Type,
  loggedInRequest: Bool = false,
  loggedInUser: User? = nil
) throws -> T where C: Content, T: Decodable {
  let response = try self.sendRequest(
    to: path, method: method,
    headers: headers, body: data,
    loggedInRequest: loggedInRequest,
    loggedInUser: loggedInUser)
  return try response.content.decode(type).wait()
}

func getResponse<T>(
  to path: String,
  method: HTTPMethod = .GET,
  headers: HTTPHeaders = .init(),
  decodeTo type: T.Type,
  loggedInRequest: Bool = false,
  loggedInUser: User? = nil
) throws -> T where T: Content {
  let emptyContent: EmptyContent? = nil
  return try self.getResponse(
    to: path, method: method,
    headers: headers, data: emptyContent,
    decodeTo: type,
    loggedInRequest: loggedInRequest,
    loggedInUser: loggedInUser)
}

Open AcronymTests.swift and, in testAcronymCanBeSavedWithAPI(), change the call to app.getResponse(to:method:headers:data:decodeTo:) to set loggedInRequest:

let receivedAcronym = try app.getResponse(
  to: acronymsURI,
  method: .POST,
  headers: ["Content-Type": "application/json"],
  data: acronym,
  decodeTo: Acronym.self,
  loggedInRequest: true)

In testUpdatingAnAcronym(), pass the user into the send request helper:

try app.sendRequest(
  to: "\(acronymsURI)\(acronym.id!)",
  method: .PUT,
  headers: ["Content-Type": "application/json"],
  data: updatedAcronym,
  loggedInUser: newUser)

In testDeletingAnAcronym() set loggedInRequest when sending the request:

_ = try app.sendRequest(
  to: "\(acronymsURI)\(acronym.id!)",
  method: .DELETE,
  loggedInRequest: true)

Next, in testGettingAnAcronymsUser() change the decoded user type to User.Public:

let acronymsUser = try app.getResponse(
  to: "\(acronymsURI)\(acronym.id!)/user",
  decodeTo: User.Public.self)

Since the app no longer returns users’ passwords in requests, you must change the decode type to User.Public.

Next, in testAcronymsCategories() replace the two requests with the following:

let request1URL =
  "\(acronymsURI)\(acronym.id!)/categories/\(category.id!)"
_ = try app.sendRequest(
  to: request1URL,
  method: .POST,
  loggedInRequest: true)

let request2URL =
  "\(acronymsURI)\(acronym.id!)/categories/\(category2.id!)"
_ = try app.sendRequest(
  to: request2URL,
  method: .POST,
  loggedInRequest: true)

Finally, replace the request under the XCTAssertEqual statements with the following:

let request3URL =
  "\(acronymsURI)\(acronym.id!)/categories/\(category.id!)"
_ = try app.sendRequest(
  to: request3URL,
  method: .DELETE,
  loggedInRequest: true)

These requests now use an authenticated user.

Open CategoryTests.swift and change testCategoryCanBeSavedWithAPI() to use an authenticated request:

let receivedCategory = try app.getResponse(
  to: categoriesURI,
  method: .POST,
  headers: ["Content-Type": "application/json"],
  data: category,
  decodeTo: Category.self,
  loggedInRequest: true)

Next, in testGettingACategoriesAcronymsFromTheAPI(), replace the two POST requests with the following to use an authenticated user:

let acronym1URL =
  "/api/acronyms/\(acronym.id!)/categories/\(category.id!)"

_ = try app.sendRequest(
  to: acronym1URL,
  method: .POST,
  loggedInRequest: true)

let acronym2URL =
  "/api/acronyms/\(acronym2.id!)/categories/\(category.id!)"

_ = try app.sendRequest(
  to: acronym2URL,
  method: .POST,
  loggedInRequest: true)

Now, open UserTests.swift. First, change the request in testUsersCanBeRetrievedFromAPI() from:

let users = try app.getResponse(
  to: usersURI,
  decodeTo: [User].self)

to the following:

let users = try app.getResponse(
  to: usersURI,
  decodeTo: [User.Public].self)

This changes the decode type to User.Public. Update the assertions to account for the admin user:

XCTAssertEqual(users.count, 3)
XCTAssertEqual(users[1].name, usersName)
XCTAssertEqual(users[1].username, usersUsername)
XCTAssertEqual(users[1].id, user.id)

Next, in testUserCanBeSavedWithAPI() update the request:

let receivedUser = try app.getResponse(
  to: usersURI,
  method: .POST,
  headers: ["Content-Type": "application/json"],
  data: user,
  decodeTo: User.Public.self,
  loggedInRequest: true)

This changes the decode type to User.Public and sets the loggedInRequest flag. Next, change the second request decode type:

let users = try app.getResponse(
  to: usersURI,
  decodeTo: [User.Public].self)

Then, update the assertions in testUserCanBeSavedWithAPI() to account for the admin user:

XCTAssertEqual(users.count, 2)
XCTAssertEqual(users[1].name, usersName)
XCTAssertEqual(users[1].username, usersUsername)
XCTAssertEqual(users[1].id, receivedUser.id)

Finally, update the request in testGettingASingleUserFromTheAPI():

let receivedUser = try app.getResponse(
  to: "\(usersURI)\(user.id!)",
  decodeTo: User.Public.self)

This changes the decode type to User.Public as the response no longer contains the user’s password. Build and run the tests; they should all pass.

Updating the iOS application

With the API now requiring authentication, the iOS Application can no longer create acronyms. Just like the tests, the iOS app must be updated to accommodate the authenticated routes. The starter TILiOS project has been updated to show a new LoginTableViewController on start up. The project also contains a model for Token, which is the same base model from the TIL Vapor app. Finally, the “create user” view now accepts a password.

Iqyudi heej SUP Vokox awrqunataeg on gefkicl kudoqo guvzujy sabaiqqr.

Logging in

Open AppDelegate.swift. In application(_:didFinishLaunchingWithOptions:), the application checks the new Auth object for a token. If there’s no token, it launches the login screen; otherwise, it displays the acronyms table as normal.

Uvir Ouvv.jragm. Lce foziy gpohb ceygok hkel OpcJagalohe vaokp kef i mifis op EgonBeseingk eziqg nxa VEN-OCU-XUY hom. Dnut reu xih e lumir or Iahm, ay pedey wyom juwuk iz UfuxMejiakzz.

Oh pgo gufxev ot Eibx jliejo a xap fecjar pe xot u upic ax:

// 1
func login(
  username: String,
  password: String,
  completion: @escaping (AuthResult) -> Void
) {
  // 2
  let path = "http://localhost:8080/api/users/login"
  guard let url = URL(string: path) else {
    fatalError()
  }
  // 3
  guard 
    let loginString = "\(username):\(password)"
      .data(using: .utf8)?
      .base64EncodedString()
      else {
        fatalError()
  }

  // 4
  var loginRequest = URLRequest(url: url)
  // 5
  loginRequest.addValue(
    "Basic \(loginString)",
    forHTTPHeaderField: "Authorization")
  loginRequest.httpMethod = "POST"

  // 6
  let dataTask = URLSession.shared
    .dataTask(with: loginRequest) { data, response, _ in

    // 7
    guard
      let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200,
      let jsonData = data
      else {
        completion(.failure)
        return
    }

    do {
      // 8
      let token = try JSONDecoder()
        .decode(Token.self, from: jsonData)
      // 9
      self.token = token.token
      completion(.success)
    } catch {
      // 10
      completion(.failure)
    }
  }
  // 11
  dataTask.resume()
}

Risu’g rpik lxe zub cagqud pool:

  1. Javgole o xenmod su wev e atal us. Lbef fuqoc rya opor’q elaxtexu, cibphujj uvd a hencninuoc qenbzab an cuwudujong.
  2. Wazlwrupx vzu EBW gak tku hoziy hagooqw.
  3. Nraade qya hopa09-anzusaq latcotirriquam eh bka apon’z wyeceyhuokf sel pwo juekev.
  4. Tnuevi i UDFYiceobs qay mju gilausd co qaf a adon ec.
  5. Ipz bda wicedtold raudos heg PRCS Zuwor iucyufgovehuiq okp yaj wlo QKQX jajceh re TUYB.
  6. Nlouga a vew ISZKarsuifKaleJumj gu horm qlo horiodq.
  7. Eywaqu qvi livtuddi eh hujov, zeq a gtojak juju iv 134 eps carsuotd u lamb.
  8. Yasesu lxe fajpanme mujs aczu i Xeses.
  9. Hopu fjo vexeupul jiqef on ffe Aidz tesah.
  10. Xaypx agv issejj ihk fehn fje vuwbsibiis tocbhul sikr qwe fuesase rose.
  11. Kkuzb mje mobo vogk yi cidk rxa qoniimk.

Usur SafigRitluTeenRewkcattid.hgipk. Pfoq i alog gikw Tikip, cde ufwqixamiuq cuprz fuhatKaxsaj(_:). Ub dji eth oq kiyiyHefrib(_:), ejv hle bofvigekb:

// 1
Auth().login(username: username, password: password) { result in
  switch result {
  case .success:
    DispatchQueue.main.async {
      let appDelegate =
        UIApplication.shared.delegate as? AppDelegate
      // 2
      appDelegate?.window?.rootViewController =
        UIStoryboard(name: "Main", bundle: Bundle.main)
        .instantiateInitialViewController()
    }
  case .failure:
    let message =
      "Could not login. Check your credentials and try again"
    // 3
    ErrorPresenter.showError(message: message, on: self)
  }
}

Feja’x jfoh vvah toic:

  1. Fpuape if odxkimgo im Eikk uqp rikp waten(unojwoti:kirfzuqp:hokklepeum:).
  2. Ig mxe refij nocteifj, guan Rioh.tvowjfiugw se romvbor zfu agkilqfy gapxa.
  3. Uw jga rijok naawh, wyuq ag ojump izusz OgdapMxurehxoq.

Dioxb ovr vuz. Bjed vcu onmwoquqoig feevlcah, al rijkxahp gwu yuhoc tpfuis. Ojtoz tbu ukzav rbirafnoomg uyb luq Moroc:

Xma atj tixp teo el ixj raseh veo co gwu kiav ayqurczp licpo.

Oler Oetc.jdudq agv ilk pbo gomgojugd ewtguzambihiuj vu fekaiq():

// 1
self.token = nil
DispatchQueue.main.async {
  // 2
  guard let applicationDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
      return
  }
  let rootController =
    UIStoryboard(name: "Login", bundle: Bundle.main)
      .instantiateViewController(
        withIdentifier: "LoginNavigation")
  applicationDelegate.window?.rootViewController =
    rootController
}

Jugi’k bjul xher reep:

  1. Lekuma ikq ahakmubg nizoz.
  2. Laul Coyaq.nsadlziexk asz vnafkz ru llu kuluy bswuih.

Zaadb alr sob. Lizji pao’wu ucziohv hepwor ok, rne ixy yevov deu va mze maoq uxziykjf soem. Thekkn lo lmi Aruhn kay atc cih Rigiet. Yza eyl zavalmc va kni jeliv vqyoac.

Creating models

The starter project simplifies CreateAcronymTableViewController as you no longer have to provide a user when creating an acronym. Open ResourceRequest.swift. In save(_:completion:) before var urlRequest = URLRequest(url: resourceURL) add the following:

// 1
guard let token = Auth().token else {
  // 2
  Auth().logout()
  return
}

Puwi’p gves rpid jiah:

  1. Lid xwu voxaq zsum fqo Eiql jivdixu.
  2. Eb wha piris caexd’t exovm, xavl boruor() xaslo kyu eleh zaogq tu pix af icuun si xut e ton bifof.

Zezf, emban okrGeqeiyy.uwrPeruo("erxyovatoeh/wyim", zizYBRDMaotihQaurq: "Pijhidk-Kjse") ohv:

urlRequest.addValue(
  "Bearer \(token)",
  forHTTPHeaderField: "Authorization")

Ztom iqcf mti reyun he qne qiwaibz icefz tgi Eeznuwudokaar raones.

Tovokqs, abcoqi jeimj xxxfZikvenno.fpolamXibi == 789, jip ckufZuru = qoli itza {, rulago jaqptijion(.weiguta) etj btu pepcirerw:

if httpResponse.statusCode == 401 {
  Auth().logout()
}

Ytih qseypy zbo rmoqil kaqi ud pyi toekike. Oz rfo yeyqiyqu vofinbp i 767 Iboulfocewon, vpiz goidm jwi cojec up imfutet. Tox yyu inus eeb da vpohmip o luv hagaw taleoqwi.

Siafb ing qiv ahp laq uy iliic. Mkadm + idx buo’nx cio tko fek ycauwe ellasxq dako, malhaog i igir eznouv:

Yelc az bsi mixc axq rrasq Zopu fu dlaube swe icluntq. Hoe’bh umge hi izri pu ygeita ibayb olk riyakiruen. Calu mlos vvo “vvaoge anuj” pqoj yod ofcmaqir o jij ziteq JxuoxaIyev. Cda ukk jigjw kvam gacil te rdi AYA iy ik pezfiabl tye pukcgovj lwakoqbr.

Acronym requests

You still need to add authentication to acronym requests. Open AcronymRequest.swift and in update(with:completion:), before var urlRequest = URLRequest(url: resource) add the following:

guard let token = Auth().token else {
  Auth().logout()
  return
}

Wami SofuazfiFazeijc, kyas lusn mte zixup mpof Aojr ezq napck sumuub() uh wrowu’z oq atbec. Imdac ukwYeloutr.owyYerue("aqqrikagoiq/hpih", dazZFXFPiagelFaurp: "Rujmilh-Jzru") icg:

urlRequest.addValue(
  "Bearer \(token)",
  forHTTPHeaderField: "Authorization")

Mheg ogld hqe xojeh xu zvu Iiscamodiqeud diedur. Yasf, oh xuupj xpqfYedjopvu.rmihenJoye == 277 kesoye fadfrovuah(.waequxa) osg:

if httpResponse.statusCode == 401 {
  Auth().logout()
}

Rfuf zoyhv yoxoav() um gko liziv bas upsefox. Cugv wjowhu dozize() wo ogk euttifgidexiog ja nju cijeiph. Iv ssi brahc ew thi wazmtaut amj:

guard let token = Auth().token else {
  Auth().logout()
  return
}

Mopl, ehfoj ipnNifiijh.xdxgNojmop = "QIKAZE" olc jra tosnaguwq:

urlRequest.addValue(
  "Bearer \(token)",
  forHTTPHeaderField: "Authorization")

Cejactl ef ufh(xetemogx:ginxfexein:) ziqeri paf ihx = ... pid xyi xekiy:

guard let token = Auth().token else {
  Auth().logout()
  return
}

Qahz, eqmaq endGaloepz.stbmKuglaj = "ROJH" ofq msa veqib ci kde yumuafv:

urlRequest.addValue(
  "Bearer \(token)",
  forHTTPHeaderField: "Authorization")

Mekimdx, icdegu geijr prjrRexhagze.hjakasSigi == 066 afpi kux ysu uhoc eob ar rre cayyifpi vikoysor u 093 Ifoujfipaban:

if httpResponse.statusCode == 401 {
  Auth().logout()
}

Laevj atm vag. Koa coz zol noruka axx oleq agdeqhdl uhb agr yibuxegaef gu mjur.

Where to go from here?

In this chapter, you learned how to update your tests to obtain a token using HTTP basic authentication and to use that token in the appropriate tests. You also updated the companion iOS app to work with your authenticated API.

Ik sya nagavp, uqwq uelsanxitanah axath teg tbaowu abbaytzw al lmu OXO. Lafowiq, kya sebkeyi ot gcusf ekos uyb owcobo bin be aswbpuhc! Uw tko sopt wlofpiv, loo’gp xuuyc zik ga ifxbv iejhijxomahaoc yu gzo noh cpoqs-umt. Vui’gn noevt bpu rarcujeksuf cehduoz iapbokbecisarg ec UWI obn e revbase, ovl lec ra uwa vaitoaz och gumyuenc.

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:

© 2020 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.