-
Notifications
You must be signed in to change notification settings - Fork 1
Authentication and Token Refresh
The strategy on the configuration decides how credentials attach; the requirement on the endpoint decides whether they must.
var config = NetworkConfiguration(baseURL: "https://api.example.com")
config.authorization = BearerAuth() // Authorization: Bearer <token from storage>
config.authorization = APIKeyAuth(key: "...", field: .header("X-API-Key"))
config.authorization = BasicAuth(username: "u", password: "p")
config.authorization = CustomAuth { request, _ in
request.setValue(sign(request), forHTTPHeaderField: "X-Signature")
}struct GetPublicFeed: Endpoint {
typealias Response = Feed
var path: String { "/feed" }
var authentication: AuthRequirement { .none } // default
}
struct GetInbox: Endpoint {
typealias Response = [Message]
var path: String { "/me/inbox" }
var authentication: AuthRequirement { .required } // must have a token; 401 triggers refresh
}Tokens live in a TokenStorage:
config.tokenStorage = InMemoryTokenStorage() // tests, or short-lived processes
config.tokenStorage = KeychainTokenStorage(service: "com.acme.app") // production; device-only by defaultKeychainTokenStorage defaults to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (not synced
to iCloud, survives backgrounding). Override with the accessibility: parameter. A custom store is
any TokenStorage conformer (SQLite, an in-house secrets manager, and so on).
A TokenPair holds accessToken, optional refreshToken, and an optional expiryDate used for
proactive refresh.
Pass a refresh handler to NetworkClient and refresh becomes automatic:
request -> 401 -> single-flight refresh -> store new TokenPair -> retry original request once
│
concurrent requests during refresh queue behind it, then resume with the new token
let client = NetworkClient(
configuration: config,
refresh: { storage in
let old = try await storage.tokenPair()
let new = try await myAuthAPI.exchange(refreshToken: old.refreshToken)
return TokenPair(accessToken: new.access, refreshToken: new.refresh, expiryDate: new.expiry)
},
onSessionExpired: { await AppRouter.logout() }
)Guarantees:
- Single-flight. Ten simultaneous 401s cause exactly one refresh call; the other nine await it.
- Queued requests. Requests started mid-refresh wait and then use the new token.
-
Loop guard. A second 401 for the same request after a fresh token surfaces
NetworkError.sessionExpiredand callsonSessionExpiredonce. -
Proactive refresh. If the stored
TokenPairhas anexpiryDate, the client refreshesproactiveRefreshLeewayseconds early (default 60) instead of waiting for a 401. -
Storage failures are not "no token". A locked keychain or an undecodable pair fails the
request instead of sending it unauthenticated, and a refreshed pair that cannot be saved fails the
refresh with
tokenRefreshFailedrather than leaving the stale token in place. -
No deadlock. The refresh request should set
skipRequestQueue = trueso a full concurrency queue plus an expired token cannot wedge.
Treat NetworkError.sessionExpired as your single "log the user out" signal. See
Error Handling and Best Practices and a Complete Example.
Authorization Code flow with PKCE. The package builds the authorization URL and exchanges the code;
the app presents the URL (typically in ASWebAuthenticationSession) and captures the redirect.
let flow = AuthorizationCodeFlow(configuration: OAuthConfiguration(
authorizationEndpoint: URL(string: "https://accounts.google.com/o/oauth2/v2/auth")!,
tokenEndpoint: URL(string: "https://oauth2.googleapis.com/token")!,
clientID: "...",
redirectURI: "myapp://callback",
scopes: ["openid", "profile", "email"]
))
let state = AuthorizationCodeFlow.makeState()
let pkce = PKCE()
let authURL = flow.authorizationURL(state: state, pkce: pkce)
// present authURL, receive redirectURL
let code = try flow.authorizationCode(fromRedirect: redirectURL, expectedState: state)
let tokens = try await flow.exchange(code: code, pkce: pkce)
// hand automatic refresh to the client:
let client = NetworkClient(configuration: config, refresh: flow.tokenManagerRefreshHandler())-
code_verifieris 43 to 128 characters from a CSPRNG;code_challengeisbase64url(SHA256(verifier))(RFC 7636). -
statemismatch on the redirect throws before any token exchange. - OAuth is entirely optional; nothing else in the package depends on it.
-
AuthorizationCodeFlowaccepts an optionallogger:so a malformed token response's decoding cause is logged even thoughOAuthError.malformedTokenResponsecan't carry it.
SwiftNetworkKit is source-available proprietary software (not open source). See FAQ Licensing and Support. Repo: https://github.com/ihusnainalii/SwiftNetworkKit · Docs: https://swiftnetworkkit.vercel.app/
Getting started
Guides
- Defining Endpoints
- Authentication and Token Refresh
- Security and Certificate Pinning
- Retry Policy
- Interceptors
- Request Management
- Caching
- Offline Request Queue
- Uploads Downloads and Progress
- Pagination and Batch
- Combine and SwiftUI
- Error Handling
- Observability
- Testing and Mocking
- Best Practices and a Complete Example
- Platform Notes, Thread Safety and Performance
Reference