swift-sdk/Sources/MCP/Base/Authorization/OAuthTokenEndpointClient.swift
Maksym Mova 6132fd4b5b
V0.12.x (#215)
* fix #203 NetworkTransport race, with main-isolated variables (#206)

Co-authored-by: wti <wti@users.noreply.github.com>

* feat/auth: Authentication implementation (#205)

* feat: added authentication

* feat(auth): improvement to error handling

* feat(auth): updated errors and tests in OAuthClientRegistrar

* feat(auth): updated ci conformance package version

* feat(auth): bumped node version

* feat(auth): updated ci conformance step

* feat(auth): improvement to Linux support

* chore: added docc plugin

* chore: added spi yml

* chore: added documentation step to ci

* chore: increased timeout for test from 5 to 10m

---------

Co-authored-by: Paal Øye-Strømme <paal.o.eye@gmail.com>
Co-authored-by: wti <wti@users.noreply.github.com>
Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
2026-03-24 11:05:22 +02:00

90 lines
3.2 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// Internal protocol for making OAuth token endpoint requests.
protocol OAuthTokenRequesting: Sendable {
func request(
parameters: inout [String: String],
endpoint: URL,
authentication: OAuthConfiguration.TokenEndpointAuthentication,
session: URLSession
) async throws -> OAuthTokenResponse
}
/// Stateless OAuth token endpoint HTTP client.
///
/// Handles the low-level HTTP mechanics of making token requests.
struct OAuthTokenEndpointClient: Sendable {
let urlValidator: OAuthURLValidator
init(urlValidator: OAuthURLValidator) {
self.urlValidator = urlValidator
}
/// Makes a token request to the given endpoint.
func request(
parameters: inout [String: String],
endpoint: URL,
authentication: OAuthConfiguration.TokenEndpointAuthentication,
session: URLSession
) async throws -> OAuthTokenResponse {
var urlRequest = URLRequest(url: endpoint)
urlRequest.httpMethod = "POST"
urlRequest.setValue(ContentType.formURLEncoded, forHTTPHeaderField: HTTPHeaderName.contentType)
urlRequest.setValue(ContentType.json, forHTTPHeaderField: HTTPHeaderName.accept)
try await authentication.apply(
to: &urlRequest,
bodyParameters: &parameters,
tokenEndpoint: endpoint
)
urlRequest.httpBody = encodeForm(parameters)
let (data, response) = try await session.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw OAuthAuthorizationError.tokenRequestFailed(statusCode: -1, oauthError: nil)
}
guard (200..<300).contains(httpResponse.statusCode) else {
let oauthError =
(try? JSONDecoder().decode(OAuthTokenErrorResponse.self, from: data))?.error
throw OAuthAuthorizationError.tokenRequestFailed(
statusCode: httpResponse.statusCode,
oauthError: oauthError
)
}
let decoded = try JSONDecoder().decode(OAuthTokenResponse.self, from: data)
guard !decoded.accessToken.isEmpty else {
throw OAuthAuthorizationError.tokenResponseInvalid
}
let tokenType = decoded.tokenType.trimmingCharacters(in: .whitespacesAndNewlines)
guard !tokenType.isEmpty,
tokenType.caseInsensitiveCompare(OAuthTokenType.bearer) == .orderedSame
else {
throw OAuthAuthorizationError.tokenResponseInvalid
}
return decoded
}
private func encodeForm(_ params: [String: String]) -> Data {
let body = params
.sorted { $0.key < $1.key }
.map { key, value in "\(percentEncode(key))=\(percentEncode(value))" }
.joined(separator: "&")
return Data(body.utf8)
}
private func percentEncode(_ string: String) -> String {
let allowed = CharacterSet(
charactersIn:
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
)
return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? string
}
}
extension OAuthTokenEndpointClient: OAuthTokenRequesting {}