Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

English | δΈ­ζ–‡

SwiftAuthNet

A lightweight, protocol-oriented authentication layer for URLSession using Swift Concurrency.

Swift 5.9+ Platforms License

Features

  • πŸ” 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

Installation

Swift Package Manager

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.

Quick Start

1. Implement TokenProvider

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")
    }
}

2. Implement TokenRefreshService

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")
    }
}

3. Configure URLSession

// 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])
)

4. Make Authenticated Requests

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

Advanced Usage

Custom Retry Conditions

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
    }
)

Per-Session Configuration

let customSession = URLSession(configuration: .default)
customSession.configureInterceptors(
    NetworkInterceptorConfig(
        interceptors: [authInterceptor],
        maxRetryCount: 2
    )
)

Multiple Interceptors

struct LoggingInterceptor: RequestInterceptor {
    func adapt(_ request: URLRequest) async throws -> URLRequest {
        print("πŸ“€ Request: \(request.url?.absoluteString ?? "")")
        return request
    }
}

URLSession.shared.configureInterceptors(
    NetworkInterceptorConfig(interceptors: [
        LoggingInterceptor(),
        authInterceptor
    ])
)

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              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)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Thread Safety

TokenRefresher uses Swift's Actor model to ensure thread safety. When multiple requests simultaneously detect an expired token:

  1. The first request starts the refresh
  2. Subsequent requests wait for the refresh to complete
  3. All requests retry with the new token

This prevents the "thundering herd" problem of multiple simultaneous refresh calls.

Requirements

  • iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+
  • Swift 5.9+
  • Xcode 15.0+

License

MIT License. See LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages