Skip to content

Instantly share code, notes, and snippets.

@fahied
Created February 13, 2025 19:57
Show Gist options
  • Save fahied/ddd6e40206af738cf3484c92fc48dea7 to your computer and use it in GitHub Desktop.
Save fahied/ddd6e40206af738cf3484c92fc48dea7 to your computer and use it in GitHub Desktop.
Bearer Token Renewal 401
import Foundation
/// A thread-safe actor responsible for managing the application's authentication token.
actor TokenManager {
/// The shared singleton instance of `TokenManager`.
static let shared = TokenManager()
/// The current authentication token.
private var token: String = "initial_token"
/// Retrieves the current authentication token.
/// - Returns: The current token as a `String`.
func getToken() -> String {
return token
}
/// Updates the authentication token with a new value.
/// - Parameter newToken: The new token to append to the current token.
func updateToken(_ newToken: String) {
token += newToken
print("token = \(token)")
}
}
/// A class responsible for handling authentication-related operations, such as token refresh.
///
/// This class ensures that if multiple requests fail with a `401 Unauthorized` error simultaneously,
/// only one token refresh operation is performed. All other requests are queued and resumed
/// once the token is successfully renewed.
///
/// ### Key Features:
/// - **Single Token Refresh**: Only one `reAuth` operation is performed, even if multiple `401` errors occur.
/// - **Queueing Requests**: Pending requests are stored in a queue and resumed after the token is refreshed.
/// - **Thread Safety**: Uses an `AsyncLock` to ensure thread-safe access to shared resources.
class Authenticator {
/// A lock to ensure that only one token refresh operation happens at a time.
private let refreshLock = AsyncLock()
/// Simulates a token refresh operation.
///
/// This method sleeps for 2 seconds to simulate a network delay and then updates the token.
/// - Throws: An error if the token refresh fails.
private func reAuth() async throws {
print("Refreshing token...")
try await Task.sleep(nanoseconds: 2_000_000_000) // Simulate network delay
await TokenManager.shared.updateToken("new_token")
print("Token refreshed.")
}
/// A no-op method used as a placeholder when no operation is needed.
/// - Throws: An error if the operation fails.
func nothing() async throws {}
/// Handles a `401 Unauthorized` error by refreshing the token if necessary.
///
/// This method ensures that only one token refresh operation happens at a time.
/// If the lock is already acquired (i.e., a token refresh is in progress),
/// the request is queued and resumed once the token is refreshed.
///
/// ### Flow:
/// 1. Check if the lock is already acquired.
/// 2. If the lock is not acquired, initiate a token refresh (`reAuth`).
/// 3. If the lock is acquired, queue the request and wait for the token refresh to complete.
/// 4. Once the token is refreshed, resume all queued requests.
///
/// - Throws: An error if the token refresh fails.
func handle401() async throws {
// Determine whether to call `reAuth` or `nothing` based on the lock state.
let ops = await refreshLock.isLocked ? nothing : reAuth
try await refreshLock.lock {
try await ops() // Ensures only one re-auth happens
}
}
}
/// A thread-safe actor that provides an asynchronous lock mechanism.
actor AsyncLock {
/// Indicates whether the lock is currently acquired.
var isLocked = false
/// A queue of continuations for tasks waiting to acquire the lock.
var waiters = [CheckedContinuation<Void, Never>]()
/// Acquires the lock, executes the given operation, and releases the lock.
/// - Parameter operation: The asynchronous operation to execute while holding the lock.
/// - Returns: The result of the operation.
/// - Throws: An error if the operation throws an error.
func lock<T>(_ operation: () async throws -> T) async throws -> T {
await acquire() // Acquire the lock
defer { release() } // Ensure the lock is released
return try await operation() // Execute the operation
}
/// Acquires the lock, suspending the task if the lock is already held.
private func acquire() async {
while isLocked {
// Suspend the task and add its continuation to the waiters queue.
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
// Yield to allow other tasks to run.
await Task.yield()
}
isLocked = true // Mark the lock as acquired
}
/// Releases the lock and resumes the next task in the waiters queue.
private func release() {
// Resume all waiting tasks.
for waiter in waiters {
waiters.removeFirst()
waiter.resume()
}
isLocked = false // Mark the lock as released
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment