Skip to content

Commit

Permalink
1.7.5
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Apr 11, 2024
1 parent 7e22dd3 commit 07106aa
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.7.4")
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.7.5")
],
targets: [
.target(
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftAPIClient/Modifiers/RateLimitModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extension APIClient {
maxRepeatCount: Int = 3
) -> Self {
waitIfRateLimitExceeded(
id: { $0.url?.baseURL?.absoluteString ?? UUID().uuidString },
id: { $0.url?.host ?? UUID().uuidString },
interval: interval,
statusCodes: statusCodes,
maxRepeatCount: maxRepeatCount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ public extension SecureCacheService where Self == MockSecureCacheService {
}
}

public extension SecureCacheService {

func save(_ date: Date, for key: SecureCacheServiceKey) async throws {
try await save(dateFormatter.string(for: date), for: key)
}

func load(for key: SecureCacheServiceKey) async -> Date? {
guard let dateString = await load(for: key) else { return nil }
return dateFormatter.date(from: dateString)
}
}

public final actor MockSecureCacheService: SecureCacheService {

private var values: [SecureCacheServiceKey: String] = [:]
Expand Down Expand Up @@ -154,3 +166,11 @@ public struct KeychainCacheService: SecureCacheService {
}
}
#endif

private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return formatter
}()
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,23 @@ public extension APIClient {
/// - Parameters:
/// - cacheService: The `SecureCacheService` to use for caching the token. Default to `.keychain`. Token must be stored with the key `.accessToken`.
/// - expiredStatusCodes: The set of status codes that indicate an expired token. Default to `[401]`.
/// - request: The closure to use for requesting a new token and refresh token first time. Set to `nil` if you want to request and cache tokens manually.
/// - request: The closure to use for requesting a new token and refresh token first time. Set to `nil` if you want to request and cache tokens manually.
/// - refresh: The closure to use for refreshing a new token with refresh token.
/// - auth: The closure that creates an `AuthModifier` for the new token. Default to `.bearer(token:)`.
///
/// - Warning: Don't use this modifier with `.auth(_ modifier:)` as it will be override it.
func tokenRefresher(
cacheService: SecureCacheService = valueFor(live: .keychain, test: .mock),
expiredStatusCodes: Set<HTTPResponse.Status> = [.unauthorized],
request: @escaping (SecureCacheService) async -> String? = { await $0.load(for: .accessToken) },
request: ((APIClient, APIClient.Configs) async throws -> (accessToken: String, refreshToken: String?, expiryDate: Date?))? = nil,
refresh: @escaping (_ refreshToken: String?, APIClient, APIClient.Configs) async throws -> (accessToken: String, refreshToken: String?, expiryDate: Date?),
auth: @escaping (String) -> AuthModifier = AuthModifier.bearer
) -> Self {
httpClientMiddleware(
TokenRefresherMiddleware(
cacheService: cacheService,
expiredStatusCodes: expiredStatusCodes,
request: request.map { request in { try await request(self, $0) } },
refresh: { try await refresh($0, self, $1) },
auth: auth
)
Expand All @@ -43,13 +44,15 @@ public extension APIClient {
func tokenRefresher(
cacheService: SecureCacheService,
expiredStatusCodes: Set<HTTPResponse.Status> = [.unauthorized],
request: ((APIClient, APIClient.Configs) async throws -> (accessToken: String, refreshToken: String?, expiryDate: Date?))? = nil,
refresh: @escaping (_ refreshToken: String?, APIClient, APIClient.Configs) async throws -> (accessToken: String, refreshToken: String?, expiryDate: Date?),
auth: @escaping (String) -> AuthModifier = AuthModifier.bearer
) -> Self {
httpClientMiddleware(
TokenRefresherMiddleware(
cacheService: cacheService,
expiredStatusCodes: expiredStatusCodes,
request: request.map { request in { try await request(self, $0) } },
refresh: { try await refresh($0, self, $1) },
auth: auth
)
Expand All @@ -63,16 +66,19 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
private let tokenCacheService: SecureCacheService
private let expiredStatusCodes: Set<HTTPResponse.Status>
private let auth: (String) -> AuthModifier
private let requestToken: ((APIClient.Configs) async throws -> (String, String?, Date?))?
private let refresh: (String?, APIClient.Configs) async throws -> (String, String?, Date?)

public init(
cacheService: SecureCacheService,
expiredStatusCodes: Set<HTTPResponse.Status> = [.unauthorized],
request: ((APIClient.Configs) async throws -> (String, String?, Date?))?,
refresh: @escaping (String?, APIClient.Configs) async throws -> (String, String?, Date?),
auth: @escaping (String) -> AuthModifier
) {
tokenCacheService = cacheService
self.refresh = refresh
self.requestToken = request
self.auth = auth
self.expiredStatusCodes = expiredStatusCodes
}
Expand All @@ -85,20 +91,30 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
guard configs.isAuthEnabled else {
return try await next(request, configs)
}
guard var accessToken = await tokenCacheService.load(for: .accessToken) else {
var accessToken: String
var currentExpiryDate: Date?
var refreshToken: String?
if let cachedToken = await tokenCacheService.load(for: .accessToken) {
accessToken = cachedToken
currentExpiryDate = await tokenCacheService.load(for: .expiryDate)
refreshToken = await tokenCacheService.load(for: .refreshToken)
} else if let requestToken, let url = request.url {
(accessToken, refreshToken, currentExpiryDate) = try await withThrowingSynchronizedAccess(id: url.host) {
try await requestToken(configs)
}
} else {
throw Errors.custom("Token not found.")
}
var refreshToken = await tokenCacheService.load(for: .refreshToken)

if
let expiryDateString = await tokenCacheService.load(for: .expiryDate),
let currentExpiryDate = dateFormatter.date(from: expiryDateString),
let currentExpiryDate,
currentExpiryDate > Date()
{
(accessToken, refreshToken, _) = try await refreshTokenAndCache(configs, accessToken: accessToken, refreshToken: refreshToken)
} else {
let token = await waitForSynchronizedAccess(id: accessToken, of: (String, String?, Date?).self)?.0
accessToken = token ?? accessToken
if let values = await waitForSynchronizedAccess(id: accessToken, of: (String, String?, Date?).self) {
(accessToken, refreshToken, currentExpiryDate) = values
}
}
var authorizedRequest = request
try auth(accessToken).modifier(&authorizedRequest, configs)
Expand All @@ -124,17 +140,9 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
try await tokenCacheService.save(refreshToken, for: .refreshToken)
}
if let expiryDate {
try await tokenCacheService.save(dateFormatter.string(from: expiryDate), for: .expiryDate)
try await tokenCacheService.save(expiryDate, for: .expiryDate)
}
return (token, refreshToken, expiryDate)
}
}
}

private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
return formatter
}()

0 comments on commit 07106aa

Please sign in to comment.