Leave a rating/review
If a session task requires authentication either as part of the requested URL or in the shared URL credential storage (and there are no valid credentials available) then the session creates an authentication challenge.
It first sends a call to didReceiveChallenge to its task delegate to handle the authentication challenge.
If the task delegate doesn’t respond to that message, the task sends didReceiveChallenge to its session delegate to handle the authentication challenge.
It’s the other way around for session-wide authentication challenges: the session calls its delegate’s didReceiveChallenge method if that method exists. Otherwise, it calls the task delegate’s didReceiveChallenge method.
Let’s take a look at how to do this in code. You’ll be connecting to a web app that was created using the Vapor framework.
It lets a user log in to add short and long forms of acronyms like TIL - which stands for Today I Learned. Anyone can get a list of all the items. I’ve already created a user, and added some acronyms.
Open the Starter project for this episode. There are some structs that match the server’s json data and model.
You’ll see these in the Acronym, Auth, and User files. There’s also AcronymView.swift that will send up an acronym when the user taps the button on-screen.
Create a new file called AcronymSender.swift. Add this class declaration in it:
class AcronymSender {
private let session: URLSession
private let sessionConfiguration: URLSessionConfiguration
init() {
self.sessionConfiguration = URLSessionConfiguration.default
self.sessionConfiguration.waitsForConnectivity = true
self.session = URLSession(configuration: sessionConfiguration)
}
}
This creates a session configuration, indicates that it should wait for a connection to be available instead of immediately failing, and then creates the session.
Both the session and configuration get stored in private constants of your class. Next, add the following properties for the necessary URLs:
private let baseURL: URL
private let loginEndpoint: URL
private let newEndpoint: URL
And then initialize the at the bottom of init:
self.baseURL = URL(string: "https://tilftw.herokuapp.com/“)!
self.loginEndpoint = URL(string: "login", relativeTo: baseURL)!
self.newEndpoint = URL(string: "new", relativeTo: baseURL)!
There is a base URL for all of your requests, a login endpoint, and the endpoint for creating a new acronym. Add a function that will be used to send up your acronym to the server:
func send(acronym: Acronym, for user: User) async throws {
}
It is an asynchronous function that can throw errors, and takes an Acronym and User as a parameter. Still within your class, add an enum for the errors you will throw.
enum AcronymError: Error {
case failedToEncodeUserCredentials
}
Then insert this code in the send method:
let credentials = "\(user.email):\(user.password)"
guard let data = credentials.data(using: .utf8) else {
throw AcronymError.failedToEncodeUserCredentials
}
let encodedString = data.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0))
You construct the credentials string as the server expects, and then attempt to create a Data object from it. If there’s an problem in the process then you throw an error, otherwise you then take that data and encode it as a Base 64 string.
To construct the URL request needed to log in in order to send an acronym you need to create a new URLRequest, set the HTTP method to POST, and set the request headers:
var loginRequest = URLRequest(url: loginEndpoint)
loginRequest.httpMethod = "POST"
loginRequest.allHTTPHeaderFields = [
"accept": "application/json",
"content-type": "application/json",
"authorization": "Basic \(encodedString)"
]
Time to perform the actual login request using the asynchronous transfer methods available:
let (loginData, loginResponse) = try await session.data(for: loginRequest)
You need to check the response to ensure everything went well
guard let httpLoginResponse = loginResponse as? HTTPURLResponse,
httpLoginResponse.statusCode == 200
else {
throw AcronymError.invalidLoginResponse
}
*Don’t forget to add the new error thrown to your enum:
case invalidLoginResponse
At this point, if things have gone well, you should have the login data object you can use to acquire the authentication token, so let’s do that:
var auth = Auth(token: "")
do {
auth = try JSONDecoder().decode(Auth.self, from: loginData)
} catch {
throw AcronymError.failedToDecodeAuthToken
}
Using JSON decoder you decode the login data object into an object of type Auth. Add the new error thrown to your enum:
case failedToDecodeAuthToken
Then construct the request to send up your new acronym to the server:
var acronymRequest = URLRequest(url: newEndpoint)
acronymRequest.httpMethod = "POST"
acronymRequest.allHTTPHeaderFields = [
"accept": "application/json",
"content-type": "application/json",
"authorization": "Bearer \(auth.token)"
]
Encode the acronym from the acronym parameter:
do {
acronymRequest.httpBody = try JSONEncoder().encode(acronym)
} catch {
throw AcronymError.failedToEncodeAcronym
}
Once again adding the new error to your enum:
case failedToEncodeAcronym
And execute the request to send the acronym:
let (_, acronymResponse) = try await session.data(for: acronymRequest)
guard let httpAcronymResponse = acronymResponse as? HTTPURLResponse,
httpAcronymResponse.statusCode == 200
else {
throw AcronymError.invalidAcronymResponse
}
Add one final error to the enum:
case invalidAcronymResponse
And you’re done! This code should look quite familiar as it’s what you’ve been learning and mastering throughout the Beginner and Advanced URLSession courses.
Open AcronymView.swift and create a new property for your acronym sender:
private let sender: AcronymSender = AcronymSender()
Next, add this code to create a User property for your authenticated requests:
private let user: User = User(email: "jo@razeware.com", name: "jo", password: "password")
Note that we hard-code these for now, since the focus is on how to work with authentication, but in a real application you’d likely acquire these from user input.
Create a function that uses your acronym sender to send up an acronym:
private func sendAcronymTapped() async {
do {
try await sender.send(acronym: acronym, for: user)
Task { @MainActor in
showAcronymSubmitSucceededAlert = true
}
} catch {
Task { @MainActor in
showAcronymSubmitFailedAlert = true
}
}
}
Which ends up being very straightforward thanks to the code you just wrote. If the task succeeds you show one alert, and if the task fails you show another one to indicate as much.
Finally, replace the print statement in your button’s action with the call to your function:
Task {
await sendAcronymTapped()
}
And you’re all done!
Build and run the app, navigate to the Acronyms tab, and tap the button. Voila! A fully authenticated request from beginning to end :) If you want to verify that your new acronym got added, open a Safari or browser window. And go to the following URL:
https://tilftw.herokuapp.com/acronyms
Awesome! You’ve now seen how to work with authenticated requests. Great work! Only a couple more topics remain, and App Transport Security is next.
So what are you waiting for? I’ll see you in the next episode!