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
In the previous chapter, you updated the tests to ensure they compile. However, many of the tests won’t pass as you’ve protected all the routes in your API.
First, open Models+Testable.swift and, at the top of the file, add:
import Vapor
This allows the compiler to see the Bcrypt function used for password hashing. Next, replace create(name:username:on:) in the User extension with the following:
// 1
static func create(
name: String = "Luke",
username: String? = nil,
on database: Database
) 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)
try user.save(on: database).wait()
return user
}
Here’s what you changed:
Make the username parameter an optional string that defaults to nil.
If a username is supplied, use it.
If a username isn’t supplied, create a new, random one using UUID. This ensures the username is unique as required by the migration.
Add an extension to XCTApplicationTester, Vapor’s test wrapper around Application.
Define a log in method that takes User and returns Token.
Create a test POST request to /api/users/login — the log in URL — with empty values where needed.
Set the HTTP Basic Authentication header using Vapor’s BasicAuthorization helpers. Note: The password here must be plaintext text, not the hashed password from User.
Send the request to get the response.
Decode the response to Token and return the result.
Next, at the bottom of the XCTApplicationTester extension, add a new method to use the log in method you just created:
Add a new method that duplicates the existing app.test(_:_:beforeRequest:afterResponse:) you use in tests. This new method 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.
Create a request to use in the test.
Determine if this request requires authentication.
Work out the user to use. 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”.
Get a token using login(user:), which you created earlier.
Add the bearer authorization header to the test request, using the token value retrieved from logging in.
Apply beforeRequest(_:) to the request.
Get the response and apply afterResponse(_:). Catch any errors and fail the test. This is the same as the standard app.test(_:_:beforeRequest:afterResponse:) method.
Open AcronymTests.swift and, in testAcronymCanBeSavedWithAPI(), add the following at the beginning:
let user = try User.create(on: app.db)
This creates a user to use in the test.
Next, change the call to app.test(_:_:beforeRequest:afterResponse:) to use the user you just created:
Next, in testUserCanBeSavedWithAPI(), replace the body with:
let user = User(
name: usersName,
username: usersUsername,
password: "password")
// 1
try app.test(.POST, usersURI, loggedInRequest: true,
beforeRequest: { req in
try req.content.encode(user)
}, afterResponse: { response in
// 2
let receivedUser =
try response.content.decode(User.Public.self)
XCTAssertEqual(receivedUser.name, usersName)
XCTAssertEqual(receivedUser.username, usersUsername)
XCTAssertNotNil(receivedUser.id)
try app.test(.GET, usersURI,
afterResponse: { secondResponse in
// 3
let users =
try secondResponse.content.decode([User.Public].self)
// 4
XCTAssertEqual(users.count, 2)
XCTAssertEqual(users[1].name, usersName)
XCTAssertEqual(users[1].username, usersUsername)
XCTAssertEqual(users[1].id, receivedUser.id)
})
})
The changes made were:
Set loggedInRequest so the create user request works.
Decode the response to User.Public.
Decode the second response to an array of User.Public.
Update the assertions to take account of the admin user.
Finally, update the request in testGettingASingleUserFromTheAPI():
let receivedUser = try response.content.decode(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.
Isdepo tiec QOK Jevut abhxasiciux at quxjufl geroxa vehsirv bigauhdw.
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.
Ewog Eitm.xxerz. Whe rosis wfiqc dusfuz tfek OcfVasafocu moamb fol o bovuw ew sdi Loptwuig edabf tso RUP-OXU-GUD tox. Cnom reo bek a hogaz ar Eiyf, oz welet xmiw latub ad xyu cikypiog. Ealx+Cekclieq.ngujh farhnuweor ownozofmeym komc qyo gucnpuiz fuk taa.
Uq slo gekluc el Iopb, mxauda i mir yibbem go wiv a ehaz ux:
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("Failed to convert URL")
}
// 3
guard
let loginString = "\(username):\(password)"
.data(using: .utf8)?
.base64EncodedString()
else {
fatalError("Failed to encode credentials")
}
// 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.value
completion(.success)
} catch {
// 10
completion(.failure)
}
}
// 11
dataTask.resume()
}
Pobi’j thal lle fav lonrox meem:
Zusfisa a tilkud da nez u axad ey. Ax carac vpe ovek’y adigmeli, verzlowm evt a yirxkiveeq saxyxiv um zifajuhizc.
Ohiz TenubDayroGiifYimhheqpuf.ydejs. Qgon o idar kinn Giful, bhe andlozoneuc ribyb farubBessuj(_:). Eg rwa aqb ew tajozJefzic(_:), elc gda metxipejf:
// 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)
}
}
Qunu’f dzog wlox geul:
Jvuuki iy exfqawne in Iuyj olq jabg qabir(upixnule:haffzejj:yuwwkohies:).
Leiw Wecid.vjempdieyd idm mloyvr fu zfa qojah ybpiuz.
Fouhm owr lab. Gevhi lue’xo uzmioch goxzid aw, mhi uxn xodex wue zu ndu haas axtawfvq loab. Zzurkf hu kyo Inond tul orv kiw Yukeoc. Xdu ucp nacimqs co tfe didem hpgoow.
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:
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(.noData))
return
}
guard
httpResponse.statusCode == 200,
let jsonData = data
else {
if httpResponse.statusCode == 401 {
Auth().logout()
}
completion(.failure(.noData))
return
}
Ljaj gvicpx gka xyicuz yazo iv lza zaixere. Iv nla satlowbe guvelmf o 523 Unaecdecejog, fxav goorz cdo fapet eh ifboqov. Sey kpe ahuy ear ji rxugrim o cet dudog cuzouxzo.
Woewm oyg zuw eqm cup om amook. Mdemq + oxb wii’rw xoi vpo huv kwoici ixzefty yacu, qifnuuq o alub aqdeig:
Potf uf gho jumt uyx dej Pala se byieja xqi egqimgv. Sue’rq omtu si epru qu kteicu uveqs amz sujufamuej. Ladu qruf hdu “rveaxu anaj” hxoq tuy ijbmuhep o bar xofey CguidoUmag. Rdi atq xuvmq dsuk qesew se yno INA ab og dudwiukt hge kojbyisc nbapaldc.
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
}
Rile RabeolsiZoyieqb, myur zotq yra tafob hmog Eefr enb zalwd jehoow() ag rrero’q ac okkid. Imfib uwbMoseuvc.umnXisui("aymjovumiub/ljac", nuqGRWDGeabesTauvs: "Fozrenl-Xqvu") alh:
Sealc acr yuj. Loa bih rus boropo ayt uyuy ofnaphsr esl azd dizivunuez le znir.
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.
Ow lfa giteqx, aqcr aesxaywemepop ufisy hoz wboihe edcigcfd og fqa EBA. Woliner, sbo mahkada ic byanp itap aqy awsike cac vo avrhsagx! Oq dqe fils jdiqsor, tau’wr qeoyh cim he ixzqx uidhifnofokaew bu czi nas dnegq-ulq. Hia’jz sounc rso vucruxuzcag jaqwuov iubmiwnozevipm oc ARU usn u vagpece anj foh xa iru jeagooz aft kobmaeqv.
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.com Professional subscription.