Authentication with signed requests in Alamofire 5

With more than 30k stars on Github, you can tell that Alamofire is a popular framework to use for iOS and Mac projects. It makes network implementations easy to do and it makes certain hard things easier, like retrying a request, authentication layers, or certificate pinning.

Alamofire 5 was released in February 2020 after being in beta for more than a year. Even though there’s reason enough to go with a simple URLSession implementation, many of us still decide to implement their network layer by making use of Alamofire.

A common thing to implement during such a network implementation is signing a request for authentication and retrying a request once it fails due to authentication. This is common with OAuth implementations and one of the reasons you could decide to go with Alamofire as it makes it a lot easier to implement such logic.

Combining a RequestAdapter with a RequestRetrier for handling authenticated requests

While building your authentication layer for network requests you’ll often need to implement logic to retry a request once you get, for example, a 401 unauthorized response code. You’ll have to refresh an existing authentication bearer or fetch an initial one.

At first, this seems to be quite a hard job to implement. However, by combining the RequestAdapter and the RequestRetrier in Alamofire this can be quite an easy job.

The RequestAdapter protocol in Alamofire is described as follows:

A type that can inspect and optionally adapt a URLRequest in some manner if necessary.

This makes it a perfect candidate for adding the authentication token as a request header.

At the same time, the RequestRetrier is described as follows:

A type that determines whether a request should be retried after being executed by the specified session manager and encountering an error.

This is perfect for catching those unauthenticated requests that fail due to a missing authentication token or due to an expired token. We can request a new authentication token and trigger a retry of the original request. This original request will then use the new token as it will be set by the request adapter.

This sounds great, right? Now that you know what we’re aiming for we can start implementing both the retrier and the adapter.

Signing requests for authentication using the RequestAdapter

APIs often require you to sign requests using JSON Web Tokens in combination with an Authorization header. Each outgoing request needs to have that authentication header set in order to be accepted by the backend.

You could add this authorization header manually every time you create the URLRequest itself. However, it’s a lot nicer to implement this in a dedicated class for signing requests. Alamofire comes with a RequestAdapter protocol that’s built exactly for these kinds of scenarios.

Every request will go through this RequestAdapter before it’s actually executed. You can decide for any request whether you want to manipulate it. In the following example, we will add the authentication header with the authentication token.

Creating a request adapter

First, you’ll need to create your own request adapter implementation. In this example, we’re adding a JSON Web Token (JWT) as an authentication header to each request that requires to be authenticated. This is done by setting the Bearer <access_token> as a value for the key Authorization.

/// The storage containing your access token, preferable a Keychain wrapper.
protocol AccessTokenStorage: class {
    typealias JWT = String
    var accessToken: JWT { get set }
}

final class RequestInterceptor: Alamofire.RequestInterceptor {

    private let storage: AccessTokenStorage

    init(storage: AccessTokenStorage) {
        self.storage = storage
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        guard urlRequest.url?.absoluteString.hasPrefix("https://api.authenticated.com") == true else {
            /// If the request requires authentication, we can directly return it as unmodified.
            return completion(.success(urlRequest))
        }
        var urlRequest = urlRequest

        /// Set the Authorization header value using the access token.
        urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")

        completion(.success(urlRequest))
    }
}

We’re making use of the RequestInterceptor protocol that provides both RequestAdapter and RequestRetrier functionality. We will need this to eventually also implement the retry functionality.

After creating the adapter class we can use it by setting up the session class as follows:

let storage = KeychainStorage()
let session = Session(interceptor: RequestInterceptor(storage: storage))

This is everything you need to authenticate your outgoing requests. The authentication header will be set for every request you’ll perform.

Creating a request retrier to retry failed requests using Alamofire’s RequestRetrier

The RequestRetrier protocol works quite similarly. We extend our RequestInterceptor and require it to refresh the token whenever we get a 401 Authorization Required response status code.

func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
    guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
        /// The request did not fail due to a 401 Unauthorized response.
        /// Return the original error and don't retry the request.
        return completion(.doNotRetryWithError(error))
    }

    refreshToken { [weak self] result in
        guard let self = self else { return }

        switch result {
        case .success(let token):
            self.storage.accessToken = token
            /// After updating the token we can safily retry the original request.
            completion(.retry)
        case .failure(let error):
            completion(.doNotRetryWithError(error))
        }
    }
}

We trigger the completion callback after we’ve refreshed the access token to retry the failed request. The request will be triggered again and succeed with the refreshed access token. I’ll leave it up to you to implement the refreshToken(_:) method as those are implementation details related to your authentication layer.

The final class looks as follows:

final class RequestInterceptor: Alamofire.RequestInterceptor {

    private let storage: AccessTokenStorage

    init(storage: AccessTokenStorage) {
        self.storage = storage
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        guard urlRequest.url?.absoluteString.hasPrefix("https://api.authenticated.com") == true else {
            /// If the request requires authentication, we can directly return it as unmodified.
            return completion(.success(urlRequest))
        }
        var urlRequest = urlRequest

        /// Set the Authorization header value using the access token.
        urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")

        completion(.success(urlRequest))
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            /// The request did not fail due to a 401 Unauthorized response.
            /// Return the original error and don't retry the request.
            return completion(.doNotRetryWithError(error))
        }

        refreshToken { [weak self] result in
            guard let self = self else { return }

            switch result {
            case .success(let token):
                self.storage.accessToken = token
                /// After updating the token we can safily retry the original request.
                completion(.retry)
            case .failure(let error):
                completion(.doNotRetryWithError(error))
            }
        }
    }
}

It includes both the RequestAdapter and RequestRetrier protocol by conforming to the RequestInterceptor protocol. It implements our complete authentication logic:

  • Set an authentication header through the RequestAdapter
  • Catch failed requests through the RequestRetrier
  • Update the token and trigger a retry if the failed request is due to a 401 Unauthorized response

And that’s it!

Conclusion

Hopefully, I’ve shown you how easy it can be to implement quite a challenging authentication layer. You only need a single interceptor class that signs requests and triggers a retry if a request fails due to a 401 Unauthorized response.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!