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(_:).