swift-microservices/swift-openapi-token-authentication
Access-token authentication and automatic refresh for clients generated by
Usage
Implement AuthenticationClient with your API's login and refresh calls. Its response must conform to AuthenticationResponse, providing access and refresh tokens and their expiration dates. Use an authentication client without this middleware for those calls to avoid waiting on its own session.
import OpenAPITokenAuthentication
let session = AuthenticationSession(
client: authenticationClient,
storage: InMemoryAuthenticationStorage<YourAuthenticationResponse>()
)
try await session.authenticate(credentials: credentials)
let client = Client(
serverURL: serverURL,
transport: transport,
middlewares: [AuthenticationMiddleware(session: session)]
)Share a session across clients using the same credentials. Access tokens refresh on demand within 30 seconds of expiration; customize this with refreshLeeway. Provide an AuthenticationStorage implementation if credentials must survive restarts.
Anonymous requests
Authentication is required by default. For endpoints that also accept anonymous requests, opt in to attaching credentials only when a session is available:
AuthenticationMiddleware(session: session, policy: .ifAvailable)The middleware first calls accessToken(), waiting for any in-flight login (including QR pairing) or refresh. If token lookup throws AuthenticationSessionError.userAuthenticationRequired, .ifAvailable sends the request unchanged without adding an Authorization header. This includes both signed-out sessions and expired or rejected refresh credentials discovered during token lookup. Existing headers are preserved; an anonymous response, including HTTP 401, is returned without attempting refresh or retry.
Other token-lookup errors, including transport failures and cancellation, propagate. Once a token has been attached, the normal authenticated refresh and retry behavior applies: a failure during the request or its HTTP 401 recovery is not retried anonymously. The server still enforces authorization for protected endpoints. accessToken() itself remains strict; only the middleware applies this policy.
This policy does not change credential validation: the session still requires valid refresh credentials when accepting a login or restoring stored credentials.
Refreshing requests
The middleware refreshes on HTTP 401 by default. Customize this with refreshableStatusCodes or a response predicate:
AuthenticationMiddleware(session: session) { response, _ in
response.status == .unauthorized
&& response.headerFields[.wwwAuthenticate]?.contains("invalid_token") == true
}Requests with single-use bodies are never retried. Only overlapping refreshes share work; a later rejected response can trigger another refresh. Cancel outstanding API requests before switching accounts, or use separate sessions for separate accounts.
The session also works without middleware:
let token = try await session.accessToken()
let refreshedToken = try await session.newAccessToken()
await session.logout()Observing state
let observation = Task {
for await state in await session.states() {
print(state) // unauthenticated, authenticating, authenticated, or refreshing
}
}
observation.cancel()Each listener receives the current state followed by changes, buffering only the latest state. Cancelling observation does not cancel authentication.
Errors and cancellation
Classify rejected credentials in your authentication client with AuthenticationClientError(.invalidCredentials) or .invalidRefreshToken, optionally preserving the API error in underlyingError. Other errors propagate unchanged.
Missing, expired, or rejected refresh credentials clear the session and throw AuthenticationSessionError.userAuthenticationRequired. Temporary refresh failures preserve valid refresh credentials so a later call can retry.
Cancelling an endpoint request does not cancel shared authentication work or immediately interrupt its wait. Logout and replacement login cancel pending work and prevent stale results from restoring credentials.
Requirements
Swift 6.3. Linux, macOS 15, iOS/Mac Catalyst/tvOS 18, watchOS 11, or visionOS 2. swift-openapi-runtime 1.12.1, swift-http-types 1.0.
Development
swift test
swift-format lint --strict --recursive Sources Tests # what the soundness check runsContributing
Pull requests are welcome. Keep a change focused, prove new behaviour with a test, and label the pull request with its semantic version impact.
License
MIT. See LICENSE.
Package Metadata
Repository: swift-microservices/swift-openapi-token-authentication
Default branch: main
README: README.md