Skip to content

Commit

Permalink
1.7.3
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Apr 11, 2024
1 parent 3c7c5fa commit 4694e7a
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 62 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.2")
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.7.3")
],
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import Foundation
/// A service for caching and retrieving secure data.
public protocol SecureCacheService {

subscript(key: SecureCacheServiceKey) -> String? { get nonmutating set }
func clear() throws
func load(for key: SecureCacheServiceKey) async -> String?
func save(_ value: String?, for key: SecureCacheServiceKey) async throws
func clear() async throws
}

/// A key for a secure cache service.
Expand Down Expand Up @@ -37,18 +38,26 @@ public extension SecureCacheService where Self == MockSecureCacheService {
}
}

public final class MockSecureCacheService: SecureCacheService {
public final actor MockSecureCacheService: SecureCacheService {

private var values: [SecureCacheServiceKey: String] = [:]

public static let shared = MockSecureCacheService()

public subscript(key: SecureCacheServiceKey) -> String? {
get { values[key] }
set { values[key] = newValue }
}
public init(_ values: [SecureCacheServiceKey: String] = [:]) {
self.values = values
}

public func load(for key: SecureCacheServiceKey) async -> String? {
values[key]
}
public func save(_ value: String?, for key: SecureCacheServiceKey) async throws {
values[key] = value
}

public func clear() throws {}
public func clear() async throws {
values.removeAll()
}
}

#if canImport(Security)
Expand Down Expand Up @@ -84,52 +93,51 @@ public struct KeychainCacheService: SecureCacheService {
self.service = service
}

public subscript(key: SecureCacheServiceKey) -> String? {
get {
// Create a query for retrieving the value
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.value,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne,
]
if let service {
query[kSecAttrService as String] = service
}

var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)

// Check the result
guard status == errSecSuccess, let data = item as? Data, let token = String(data: data, encoding: .utf8) else {
return nil
}

return token
}
nonmutating set {
// Create a query for saving the token
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.value,
]

if let service {
query[kSecAttrService as String] = service
}

// Try to delete the old value if it exists
SecItemDelete(query as CFDictionary)

if let newValue {
query[kSecValueData as String] = newValue.data(using: .utf8)
// Add the new token to the Keychain
SecItemAdd(query as CFDictionary, nil)
// Check the result
// status == errSecSuccess
}
}
}
public func load(for key: SecureCacheServiceKey) async -> String? {
// Create a query for retrieving the value
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.value,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne,
]
if let service {
query[kSecAttrService as String] = service
}

var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)

// Check the result
guard status == errSecSuccess, let data = item as? Data, let token = String(data: data, encoding: .utf8) else {
return nil
}

return token
}

public func save(_ value: String?, for key: SecureCacheServiceKey) async throws {
// Create a query for saving the token
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.value,
]

if let service {
query[kSecAttrService as String] = service
}

// Try to delete the old value if it exists
SecItemDelete(query as CFDictionary)

if let value {
query[kSecValueData as String] = value.data(using: .utf8)
// Add the new token to the Keychain
SecItemAdd(query as CFDictionary, nil)
// Check the result
// status == errSecSuccess
}
}

public func clear() throws {
var query: [String: Any] = [kSecClass as String: kSecClassGenericPassword]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public extension APIClient {
func tokenRefresher(
cacheService: SecureCacheService = valueFor(live: .keychain, test: .mock),
expiredStatusCodes: Set<HTTPResponse.Status> = [.unauthorized],
request: @escaping (SecureCacheService) -> String? = { $0[.accessToken] },
request: @escaping (SecureCacheService) async -> String? = { await $0.load(for: .accessToken) },
refresh: @escaping (_ refreshToken: String?, APIClient, APIClient.Configs) async throws -> (accessToken: String, refreshToken: String?, expiryDate: Date?),
auth: @escaping (String) -> AuthModifier = AuthModifier.bearer
) -> Self {
Expand Down Expand Up @@ -85,13 +85,13 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
guard configs.isAuthEnabled else {
return try await next(request, configs)
}
guard var accessToken = tokenCacheService[.accessToken] else {
guard var accessToken = await tokenCacheService.load(for: .accessToken) else {
throw Errors.custom("Token not found.")
}
var refreshToken = tokenCacheService[.refreshToken]
var refreshToken = await tokenCacheService.load(for: .refreshToken)

if
let expiryDateString = tokenCacheService[.expiryDate],
let expiryDateString = await tokenCacheService.load(for: .expiryDate),
let currentExpiryDate = dateFormatter.date(from: expiryDateString),
currentExpiryDate > Date()
{
Expand Down Expand Up @@ -119,12 +119,12 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
) async throws -> (String, String?, Date?) {
try await withThrowingSynchronizedAccess(id: accessToken) { [self] in
let (token, refreshToken, expiryDate) = try await refresh(refreshToken, configs)
tokenCacheService[.accessToken] = token
try await tokenCacheService.save(token, for: .accessToken)
if let refreshToken {
tokenCacheService[.refreshToken] = refreshToken
try await tokenCacheService.save(refreshToken, for: .refreshToken)
}
if let expiryDate {
tokenCacheService[.expiryDate] = dateFormatter.string(from: expiryDate)
try await tokenCacheService.save(dateFormatter.string(from: expiryDate), for: .expiryDate)
}
return (token, refreshToken, expiryDate)
}
Expand Down

0 comments on commit 4694e7a

Please sign in to comment.