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 does not require 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 safely 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 does not require 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 safely 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!