HTTP in Swift, Part 13: Basic Authentication

Part 13 in a series on building a Swift HTTP framework:

  1. HTTP in Swift, Part 1: An Intro to HTTP
  2. HTTP in Swift, Part 2: Basic Structures
  3. HTTP in Swift, Part 3: Request Bodies
  4. HTTP in Swift, Part 4: Loading Requests
  5. HTTP in Swift, Part 5: Testing and Mocking
  6. HTTP in Swift, Part 6: Chaining Loaders
  7. HTTP in Swift, Part 7: Dynamically Modifying Requests
  8. HTTP in Swift, Part 8: Request Options
  9. HTTP in Swift, Part 9: Resetting
  10. HTTP in Swift, Part 10: Cancellation
  11. HTTP in Swift, Part 11: Throttling
  12. HTTP in Swift, Part 12: Retrying
  13. HTTP in Swift, Part 13: Basic Authentication
  14. HTTP in Swift, Part 14: OAuth Setup
  15. HTTP in Swift, Part 15: OAuth
  16. HTTP in Swift, Part 16: Composite Loaders
  17. HTTP in Swift, Part 17: Brain Dump
  18. 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 a user and password. Since HTTPRequest uses URLComponents under-the-hood, how might you change this loader to look there for possible authorization information, instead of (or in addition to) the basicCredentials 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 or 403 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.


Related️️ Posts️

HTTP in Swift, Part 18: Wrapping Up
HTTP in Swift, Part 17: Brain Dump
HTTP in Swift, Part 16: Composite Loaders
HTTP in Swift, Part 15: OAuth
HTTP in Swift, Part 14: OAuth Setup
HTTP in Swift, Part 12: Retrying
HTTP in Swift, Part 11: Throttling
HTTP in Swift, Part 10: Cancellation
HTTP in Swift, Part 9: Resetting
HTTP in Swift, Part 8: Request Options
HTTP in Swift, Part 7: Dynamically Modifying Requests
HTTP in Swift, Part 6: Chaining Loaders
HTTP in Swift, Part 5: Testing and Mocking
HTTP in Swift, Part 4: Loading Requests
HTTP in Swift, Part 3: Request Bodies
HTTP in Swift, Part 2: Basic Structures
HTTP in Swift, Part 1: An Intro to HTTP