A lightweight, protocol-oriented authentication layer for URLSession using Swift Concurrency.
- π Automatic Bearer token injection - Seamlessly adds authentication headers
- π Transparent 401 handling - Automatically refreshes tokens and retries requests
- π§΅ Thread-safe refresh - Uses Swift Actors to prevent duplicate refresh calls
- π§© Protocol-oriented - Easy to test and customize
- π¦ Zero dependencies - Only requires Foundation
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/yvente/SwiftAuthNet.git", from: "0.1.0")
]Or in Xcode: File β Add Package Dependencies β Enter the repository URL.
import SwiftAuthNet
final class KeychainTokenProvider: TokenProvider {
func getAccessToken() async -> String? {
return Keychain.get("access_token")
}
func getRefreshToken() async -> String? {
return Keychain.get("refresh_token")
}
func clearTokens() async {
Keychain.delete("access_token")
Keychain.delete("refresh_token")
}
}final class AuthRefreshService: TokenRefreshService {
private let tokenProvider: TokenProvider
init(tokenProvider: TokenProvider) {
self.tokenProvider = tokenProvider
}
func refreshToken() async throws {
guard let refreshToken = await tokenProvider.getRefreshToken() else {
throw AuthError.noRefreshToken
}
var request = URLRequest(url: URL(string: "https://api.example.com/auth/refresh")!)
request.httpMethod = "POST"
request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization")
// Use plain URLSession to avoid interceptor loop
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw AuthError.refreshFailed
}
let tokens = try JSONDecoder().decode(TokenResponse.self, from: data)
Keychain.set(tokens.accessToken, for: "access_token")
Keychain.set(tokens.refreshToken, for: "refresh_token")
}
}// Create components
let tokenProvider = KeychainTokenProvider()
let refreshService = AuthRefreshService(tokenProvider: tokenProvider)
let refresher = TokenRefresher(refreshService: refreshService)
// Create interceptor
let authInterceptor = AuthInterceptor(
tokenProvider: tokenProvider,
tokenRefresher: refresher
)
// Configure globally
URLSession.shared.configureInterceptors(
NetworkInterceptorConfig(interceptors: [authInterceptor])
)let request = URLRequest(url: URL(string: "https://api.example.com/user/profile")!)
let (data, response) = try await URLSession.shared.authenticatedRequest(request)That's it! The library automatically:
- Adds the Bearer token to your request
- Detects 401 responses
- Refreshes the token
- Retries the original request
let interceptor = AuthInterceptor(
tokenProvider: tokenProvider,
tokenRefresher: refresher,
shouldRefreshOnResponse: { response in
// Refresh on 401 or 403
return response.statusCode == 401 || response.statusCode == 403
},
shouldClearTokensOnRefreshFailure: { error in
// Clear tokens if refresh returns 401
if case NetworkError.unauthorized = error {
return true
}
return false
}
)let customSession = URLSession(configuration: .default)
customSession.configureInterceptors(
NetworkInterceptorConfig(
interceptors: [authInterceptor],
maxRetryCount: 2
)
)struct LoggingInterceptor: RequestInterceptor {
func adapt(_ request: URLRequest) async throws -> URLRequest {
print("π€ Request: \(request.url?.absoluteString ?? "")")
return request
}
}
URLSession.shared.configureInterceptors(
NetworkInterceptorConfig(interceptors: [
LoggingInterceptor(),
authInterceptor
])
)ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β URLSession.authenticatedRequest() β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NetworkInterceptorConfig β
β (Interceptors, Retry Policy, Max Retries) β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AuthInterceptor β
β adapt(): Inject Bearer Token β
β retry(): Handle 401, Refresh Token β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
ββββββββββββββββ΄βββββββββββββββ
βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββ
β TokenProvider β β TokenRefresher β
β (Read/Store Token) β β (Actor: Safe Refresh)β
βββββββββββββββββββββββ βββββββββββββββββββββββ
TokenRefresher uses Swift's Actor model to ensure thread safety. When multiple requests simultaneously detect an expired token:
- The first request starts the refresh
- Subsequent requests wait for the refresh to complete
- All requests retry with the new token
This prevents the "thundering herd" problem of multiple simultaneous refresh calls.
- iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+
- Swift 5.9+
- Xcode 15.0+
MIT License. See LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.