HTTP in Swift, Part 13: Basic Authentication
Part 13 in a series on building a Swift HTTP framework:
- HTTP in Swift, Part 1: An Intro to HTTP
- HTTP in Swift, Part 2: Basic Structures
- HTTP in Swift, Part 3: Request Bodies
- HTTP in Swift, Part 4: Loading Requests
- HTTP in Swift, Part 5: Testing and Mocking
- HTTP in Swift, Part 6: Chaining Loaders
- HTTP in Swift, Part 7: Dynamically Modifying Requests
- HTTP in Swift, Part 8: Request Options
- HTTP in Swift, Part 9: Resetting
- HTTP in Swift, Part 10: Cancellation
- HTTP in Swift, Part 11: Throttling
- HTTP in Swift, Part 12: Retrying
- HTTP in Swift, Part 13: Basic Authentication
- HTTP in Swift, Part 14: OAuth Setup
- HTTP in Swift, Part 15: OAuth
- HTTP in Swift, Part 16: Composite Loaders
- HTTP in Swift, Part 17: Brain Dump
- HTTP in Swift, Part 18: Wrapping Up
HTTP requests to web apis frequently need to have some sort of credential to go with them. The simplest kind of authentication is Basic Access authentication, and in this post we’ll be adding this feature to our library.
When we read through the specification (or Wikipedia article) for Basic authentication, we see that it is simply the addition of a single Authorization
header, with a base64-encoded username and password. Adding header values is something we’ve seen before, and so our BasicAuth
loader will be similar in principle to our ApplyEnvironment
loader.
The Setup
The first thing we’ll need is a struct to represent the username and password:
public struct BasicCredentials: Hashable, Codable {
public let username: String
public let password: String
public init(username: String, password: String) { ... }
}
We’ll also want an HTTPRequestOption
so that an individual request can specify unique credentials:
extension BasicCredentials: HTTPRequestOption {
public static let defaultOptionValue: BasicCredentials? = nil
}
extension HTTPRequest {
public var basicCredentials: BasicCredentials? {
get { self[option: BasicCredentials.self] }
set { self[option: BasicCredentials.self] = newValue }
}
}
With that out of the way, let’s turn to the loader.
The Loader
The loader’s behavior is simple: when a request comes in, the loader sees if it specifies custom credentials. If so, it transforms them into the proper Authorization
header and sends the request down the chain. If it doesn’t, it applies any locally-stored credentials to the request and then sends it on. If it doesn’t have any credentials, it pauses incoming requests while it retrieves some.
Let’s stub this out:
public protocol BasicAuthDelegate: AnyObject {
func basicAuth(_ loader: BasicAuth, retrieveCredentials callback: @escaping (BasicCredentials?) -> Void)
}
public class BasicAuth: HTTPLoader {
public weak var delegate: BasicAuthDelegate?
private var credentials: BasicCredentials?
private var pendingTasks = Array<HTTPTask>()
public override func load(task: HTTPTask) {
if let customCredentials = task.request.basicCredentials {
self.apply(customCredentials, to: task)
} else if let mainCredentials = credentials {
self.apply(mainCredentials, to: task)
} else {
self.pendingTasks.append(task)
// TODO: ask delegate for credentials
}
}
private func apply(_ credentials: BasicCredentials, to task: HTTPTask) {
let joined = credentials.username + ":" + credentials.password
let data = Data(joined.utf8)
let encoded = data.base64EncodedString()
let header = "Basic \(encoded)"
task.request[header: "Authorization"] = header
}
}
Food for thought: The delegate method asynchronously asks for credentials instead of using a synchronous return value. Why?
This is the basic implementation, but it’s not quite right. For example, if many requests are coming in all at once, we’ll end up asking the delegate many times for credentials. We’ll need to do a little bit better with managing some state:
public class BasicAuth: HTTPLoader {
public weak var delegate: BasicAuthDelegate?
private enum State {
case idle
case retrievingCredentials(Array<HTTPTask>)
case authorized(BasicCredentials)
}
private var state = State.idle
public override func load(task: HTTPTask) {
if let customCredentials = task.request.basicCredentials {
self.apply(customCredentials, to: task)
super.load(task: task)
return
}
// TODO: make this threadsafe
switch state {
case .idle:
// we need to ask for credentials
self.state = .retrievingCredentials([task])
self.retrieveCredentials()
case .retrievingCredentials(let others):
// we are currently asking for credentials and waiting for the delegate
self.state = .retrievingCredentials(others + [task])
case .authorized(let credentials):
// we have credentials
self.apply(credentials, to: task)
super.load(task: task)
}
}
private func retrieveCredentials() {
if let d = delegate {
// we've got a delegate! Ask it for credentials
// these credentials could come from storage (eg, the Keychain), from a UI ("log in" page), or somewhere else
// that decision is entirely up to the delegate
DispatchQueue.main.async {
d.basicAuth(self, retrieveCredentials: { self.processCredentials($0) })
}
} else {
// we don't have a delegate. Assume "nil" credentials
self.processCredentials(nil)
}
}
private func processCredentials(_ retrieved: BasicCredentials?) {
// TODO: make this threadsafe
guard case .retrievingCredentials(let pending) = state else {
// we got credentials, but weren't waiting for them; do nothing
// this could happen if we were "reset()" while waiting for the delegate
return
}
if let credentials = retrieved {
state = .authorized(credentials)
for task in pending {
self.apply(credentials, to: task)
super.load(task: task)
}
} else {
// we asked for credentials but didn't get any
// all of these tasks will fail
state = .idle
pending.forEach { $0.fail(.cannotAuthenticate) }
}
}
private func apply(_ credentials: BasicCredentials, to task: HTTPTask) {
...
}
}
This is looking a lot better, but it’s still missing some key things, which I’ll leave up to you to implement:
- We need to watch for a task getting cancelled while it’s pending (ie, the
.retrievingCredentials
state) - None of this is thread-safe
- We need the
reset(with:)
logic to fail pending tasks and move back to the.idle
state
There are also some other scenarios that are worth considering:
Food for thought:
This loader assumes that a request will always be authenticated. How would you change it to allow requests to bypass authentication entirely because they don’t need it?
There’s a way on
URLComponents
to specify auser
andpassword
. SinceHTTPRequest
usesURLComponents
under-the-hood, how might you change this loader to look there for possible authorization information, instead of (or in addition to) thebasicCredentials
request option? Do you think this should be the API instead? Why or why not?This loader does not detect when a request fails authentication, such as a response coming back with
401 Unauthorized
or403 Forbidden
. Should this loader try and detect that? Why or why not?
As we’ve seen, basic authentication comes down to the simple addition of a single header to our outgoing request. By delaying incoming requests, we can turn around and ask another part of the app (via a delegate) if it has any credentials for us to use.
This pattern will continue to serve us in the next post, when we’ll look at the much more complicated scenario of authentication via OAuth 2.0.