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 ### 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. 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 ### 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. 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.URL
import struct Foundation.URLComponents
import struct Foundation.URLQueryItem
import struct HTTPTypes.HTTPField
import struct HTTPTypes.HTTPFields
import struct HTTPTypes.HTTPRequest import struct HTTPTypes.HTTPRequest
import struct HTTPTypes.HTTPResponse import struct HTTPTypes.HTTPResponse
import protocol OpenAPIRuntime.ClientMiddleware import protocol OpenAPIRuntime.ClientMiddleware
@@ -30,21 +26,43 @@ import class OpenAPIRuntime.HTTPBody
/// ///
/// ## Usage /// ## Usage
/// ///
/// Provide a fixed token:
///
/// ```swift /// ```swift
/// let middleware = BearerAuthMiddleware(token: "your-jwt-token") /// 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 { public struct BearerAuthMiddleware {
// MARK: Properties // MARK: Properties
/// The Bearer token to include in the `Authorization` header of each request. /// The closure that provides the Bearer token for each request.
private let token: String private let tokenProvider: @Sendable () async throws -> String
// MARK: Initializers // MARK: Initializers
/// Creates a new middleware instance with the given Bearer token. /// Creates a new middleware instance with a fixed Bearer token.
/// - Parameter token: A JSON Web Token (JWT) string used to authenticate requests to the App Store Connect API. /// - Parameter token: A JSON Web Token (JWT) used to authenticate requests to the App Store Connect API.
init(token: String) { public init(
self.token = token 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, operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?) { ) async throws -> (HTTPResponse, HTTPBody?) {
let token = try await tokenProvider()
var request = request var request = request
request.headerFields[.authorization] = "Bearer \(token)" request.headerFields[.authorization] = "Bearer \(token)"
@@ -3,7 +3,8 @@ import HTTPTypes
import OpenAPIRuntime import OpenAPIRuntime
import Testing import Testing
@testable import ASConnectService // A non-testable import ensures that the middleware can be created from outside the module.
import ASConnectService
@Suite("BearerAuthMiddleware") @Suite("BearerAuthMiddleware")
struct BearerAuthMiddlewareTests { 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 // MARK: - HTTPRequest+Samples