Support token providers in Bearer Auth middleware.

This commit is contained in:
javier committed 2026-10-25 17:04:22 +01:00
1 parent 2c0c050e30
commit d5035f9a3a
4 files changed
+109 -11

No files matched your search

+8
View File
@@ -54,6 +54,14 @@ let client = Client(
)
```
Tokens expire after 20 minutes at most. For longer-lived clients, pass a token provider instead, which is called on every request:
```swift
BearerAuthMiddleware {
try await tokenStore.validToken()
}
```
### Making API Calls
The `Client` conforms to `APIProtocol`, which defines a method for every endpoint in the App Store Connect API. Each method accepts an `Input` value and returns an `Output` value with the response.
@@ -25,6 +25,14 @@ let client = Client(
)
```
Tokens expire after 20 minutes at most. For longer-lived clients, pass a token provider instead, which is called on every request:
```swift
BearerAuthMiddleware {
try await tokenStore.validToken()
}
```
### Making API Calls
The ``Client`` conforms to ``APIProtocol``, which defines a method for every endpoint in the App Store Connect API. Each method accepts an `Input` value and returns an `Output` value with the response.
@@ -13,10 +13,6 @@
// ===----------------------------------------------------------------------===
import struct Foundation.URL
import struct Foundation.URLComponents
import struct Foundation.URLQueryItem
import struct HTTPTypes.HTTPField
import struct HTTPTypes.HTTPFields
import struct HTTPTypes.HTTPRequest
import struct HTTPTypes.HTTPResponse
import protocol OpenAPIRuntime.ClientMiddleware
@@ -30,21 +26,43 @@ import class OpenAPIRuntime.HTTPBody
///
/// ## Usage
///
/// Provide a fixed token:
///
/// ```swift
/// let middleware = BearerAuthMiddleware(token: "your-jwt-token")
/// ```
///
/// Tokens expire after 20 minutes at most, so long-lived clients should provide a token provider instead:
///
/// ```swift
/// let middleware = BearerAuthMiddleware {
/// try await tokenStore.validToken()
/// }
/// ```
public struct BearerAuthMiddleware {
// MARK: Properties
/// The Bearer token to include in the `Authorization` header of each request.
private let token: String
/// The closure that provides the Bearer token for each request.
private let tokenProvider: @Sendable () async throws -> String
// MARK: Initializers
/// Creates a new middleware instance with the given Bearer token.
/// - Parameter token: A JSON Web Token (JWT) string used to authenticate requests to the App Store Connect API.
init(token: String) {
self.token = token
/// Creates a new middleware instance with a fixed Bearer token.
/// - Parameter token: A JSON Web Token (JWT) used to authenticate requests to the App Store Connect API.
public init(
token: String
) {
self.init { token }
}
/// Creates a new middleware instance that asks for a Bearer token on every request.
///
/// If the provider throws, the error is rethrown and the request is not sent.
/// - Parameter tokenProvider: A closure that returns a valid JSON Web Token (JWT).
public init(
tokenProvider: @escaping @Sendable () async throws -> String
) {
self.tokenProvider = tokenProvider
}
}
@@ -68,6 +86,8 @@ extension BearerAuthMiddleware: ClientMiddleware {
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) {
let token = try await tokenProvider()
var request = request
request.headerFields[.authorization] = "Bearer \(token)"
@@ -3,7 +3,8 @@ import HTTPTypes
import OpenAPIRuntime
import Testing
@testable import ASConnectService
// A non-testable import ensures that the middleware can be created from outside the module.
import ASConnectService
@Suite("BearerAuthMiddleware")
struct BearerAuthMiddlewareTests {
@@ -112,6 +113,67 @@ struct BearerAuthMiddlewareTests {
}
)
}
@Test("Asks the token provider for a token on every request")
func asksTokenProviderOnEveryRequest() async throws {
let tokens = TokenSequence()
let middleware = BearerAuthMiddleware {
await tokens.next()
}
for expectedToken in ["token-1", "token-2"] {
_ = try await middleware.intercept(
.get,
body: nil,
baseURL: .base,
operationID: "listApps",
next: { interceptedRequest, _, _ in
#expect(interceptedRequest.headerFields[.authorization] == "Bearer \(expectedToken)")
return (HTTPResponse(status: .ok), nil)
}
)
}
}
@Test("Rethrows the token provider error without calling the next handler")
func rethrowsTokenProviderError() async throws {
let middleware = BearerAuthMiddleware {
throw TokenError.expired
}
await #expect(throws: TokenError.expired) {
_ = try await middleware.intercept(
.get,
body: nil,
baseURL: .base,
operationID: "listApps",
next: { _, _, _ in
Issue.record("The next handler should not be called when the token provider throws")
return (HTTPResponse(status: .ok), nil)
}
)
}
}
}
// MARK: - Helpers
/// An actor that returns a new token every time it is asked for one.
private actor TokenSequence {
private var count = 0
func next() -> String {
count += 1
return "token-\(count)"
}
}
/// An error thrown by a token provider.
private enum TokenError: Error {
case expired
}
// MARK: - HTTPRequest+Samples