Support token providers in Bearer Auth middleware.
This commit is contained in:
1 parent
2c0c050e30
commit
d5035f9a3a
4 files changed
+109
-11
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user