Hello :)
I've created a proposal for an additional feature and would appreciate your feedback.
Introduction
Provides an implementation of APIClientDelegate specifically for authenticating requests.
Motivation
Alamofire provides an implementation called AuthenticationInterceptor
for authentication. (docs)
AuthenticationInterceptor
takes care of state management when sending concurrent requests.
Therefore, users only need to implement each type that conforms to the AuthenticationCredential
protocol and the Authenticator
protocol.
I would like to see an implementation of AuthenticationInterceptor
that manages authentication in Get
as well.
(With an implementation based on async/await and actor, of course.)
Implementation
Authenticator
Types adopting the Authenticator
protocol will load or update Credential
and apply the Credential
to URLRequest
.
AuthenticationInterceptor
The AuthenticationInterceptor
class provides authentication for requests using exclusive control. It relies on an Authenticator
type to handle the actual URLRequest
authentication and Credential
refresh.
AuthenticationInterceptor uses actor State
, for exclusion control, and can apply and refresh authentication in order even for parallel requests.
Sample Usage
- Implement a class that adapt to the
Authenticator
protocol.
public class SampleAuthenticator: Authenticator {
public typealias Credential = Token
public var tokenStore: TokenStore
public let client: APIClient
public init(tokenStore: TokenStore, clientToRefreshCredential: APIClient) {
self.tokenStore = tokenStore
self.client = clientToRefreshCredential
}
public func credential() async throws -> Token {
if let token = tokenStore.token, token.expiresDate < Date() {
return token
}
// If there is no token, generate a token.
let token: Token = try await client.send(.post("/token")).value
tokenStore.save(token)
return token
}
public func apply(_ credential: Token, to request: inout URLRequest) async throws {
request.setValue(authorizationHeaderValue(for: credential), forHTTPHeaderField: "Authorization")
}
public func refresh(_ credential: Credential) async throws -> Credential {
let token: Token = try await client.send(.put("/token", body: ["refresh_token": credential.refreshToken])).value
tokenStore.save(token)
return token
}
public func didRequest(_: URLRequest, failDueToAuthenticationError error: Error) -> Bool {
if case .unacceptableStatusCode(let status) = (error as? APIError), status == 401 {
return true
}
return false
}
public func isRequest(_ request: URLRequest, authenticatedWith credential: Token) -> Bool {
request.value(forHTTPHeaderField: "Authorization") == authorizationHeaderValue(for: credential)
}
private func authorizationHeaderValue(for token: Token) -> String {
"token \(token.accessToken)"
}
}
- Set
AuthenticationInterceptor
with SampleAuthenticator
as APIClientDelegate
let authenticator = SampleAuthenticator(tokenStore: TokenStore(),
clientToRefreshCredential: APIClient(host: "example.com"))
let apiClient = APIClient(host: "example.com") {
$0.delegate = AuthenticationInterceptor(authenticator: authenticator)
}
Impact on existing packages
Breaking Changes
Changed method shouldClientRetry(_ client: APIClient, withError error: Error) async throws -> Bool
to shouldClientRetry(_ client: APIClient, for request: URLRequest, with error: Error) async throws -> Bool
in APIClientDelegate
because URLRequest
was needed to manage retries for parallel requests.
Other Changes
In order to pass the URLRequest
actually sent to APIClientDelegate.shouldClientRetry(_:for:with:)
, APIClientDelegate.client(_:willSendRequest:)
is now called from APIClient.send(_:)
to call it from APIClient.send(_:)
instead of APIClient.actuallySend(_:)
.