swift-sdk/Sources/MCP/Base/Authorization/OAuthModels.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

247 lines
8.8 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct HTTPAuthenticationChallengeError: Error {
let statusCode: Int
let headers: [String: String]
}
/// Parsed representation of a `WWW-Authenticate: Bearer` challenge header.
///
/// Servers return this challenge in `401 Unauthorized` and `403 Forbidden` responses
/// to indicate that a Bearer token is required or that the presented token lacks
/// sufficient scope.
///
/// The ``OAuthWWWAuthenticateParsing`` protocol produces instances of this type.
public struct OAuthBearerChallenge: Sendable {
/// Raw key-value parameters extracted from the `Bearer` challenge.
public let parameters: [String: String]
/// Creates a challenge from raw parsed parameters.
/// - Parameter parameters: Key-value pairs from the `WWW-Authenticate` header.
public init(parameters: [String: String]) {
self.parameters = parameters
}
/// The `resource_metadata` parameter, parsed as a URL.
///
/// Points to the server's RFC 9728 Protected Resource Metadata document.
/// When present, ``OAuthAuthorizer`` uses this URL as the highest-priority
/// discovery candidate.
public var resourceMetadataURL: URL? {
guard let value = parameters["resource_metadata"] else { return nil }
return URL(string: value)
}
/// The `scope` parameter from the Bearer challenge.
///
/// Specifies the scopes required or recommended for this resource.
/// ``OAuthAuthorizer`` uses this value as the highest-priority scope hint.
public var scope: String? {
parameters["scope"]
}
/// The `error` parameter from the Bearer challenge (e.g., `"invalid_token"`, `"insufficient_scope"`).
public var error: String? {
parameters["error"]
}
/// The `error_description` parameter from the Bearer challenge.
public var errorDescription: String? {
parameters["error_description"]
}
}
/// RFC9728 OAuth Protected Resource metadata (client-side, decode-only).
struct OAuthProtectedResourceMetadata: Decodable, Sendable, Equatable {
let resource: String?
let authorizationServers: [URL]
let scopesSupported: [String]?
enum CodingKeys: String, CodingKey {
case resource
case authorizationServers = "authorization_servers"
case scopesSupported = "scopes_supported"
}
}
/// RFC8414/OIDC authorization server metadata.
struct OAuthAuthorizationServerMetadata: Decodable, Sendable, Equatable {
let issuer: URL?
let authorizationEndpoint: URL?
let tokenEndpoint: URL?
let registrationEndpoint: URL?
let codeChallengeMethodsSupported: [String]?
let tokenEndpointAuthMethodsSupported: [String]?
let clientIDMetadataDocumentSupported: Bool?
enum CodingKeys: String, CodingKey {
case issuer
case authorizationEndpoint = "authorization_endpoint"
case tokenEndpoint = "token_endpoint"
case registrationEndpoint = "registration_endpoint"
case codeChallengeMethodsSupported = "code_challenge_methods_supported"
case tokenEndpointAuthMethodsSupported = "token_endpoint_auth_methods_supported"
case clientIDMetadataDocumentSupported = "client_id_metadata_document_supported"
}
}
struct OAuthTokenResponse: Decodable, Sendable, Equatable {
let accessToken: String
let tokenType: String
let expiresIn: Int?
let scope: String?
let refreshToken: String?
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case tokenType = "token_type"
case expiresIn = "expires_in"
case scope
case refreshToken = "refresh_token"
}
}
struct OAuthTokenErrorResponse: Decodable {
let error: String
}
/// An OAuth 2.1 access token and its associated metadata.
///
/// Stored by ``TokenStorage`` and produced by ``OAuthAuthorizer`` after a successful
/// token request. Use ``isExpired(now:skewSeconds:)`` to check validity before use.
public struct OAuthAccessToken: Sendable {
/// The raw bearer token string for use in the `Authorization` header.
public let value: String
/// The token type returned by the authorization server (should be `"Bearer"`).
public let tokenType: String
/// The UTC date after which the token is considered expired, or `nil` if no expiry was specified.
public let expiresAt: Date?
/// The set of OAuth scopes granted with this token.
public let scopes: Set<String>
/// The issuer URL of the authorization server that issued this token.
///
/// Used to detect when the active authorization server changes between requests,
/// triggering a token invalidation.
public let authorizationServer: URL?
/// The refresh token, if the authorization server issued one alongside the access token.
public let refreshToken: String?
/// Creates a new access token record.
public init(
value: String,
tokenType: String,
expiresAt: Date?,
scopes: Set<String>,
authorizationServer: URL?,
refreshToken: String?
) {
self.value = value
self.tokenType = tokenType
self.expiresAt = expiresAt
self.scopes = scopes
self.authorizationServer = authorizationServer
self.refreshToken = refreshToken
}
/// Returns `true` if the token has expired or will expire within the skew window.
///
/// - Parameters:
/// - now: The reference time to compare against. Defaults to `Date()`.
/// - skewSeconds: Clock skew buffer in seconds. Defaults to ``OAuthTokenExpirySkew/defaultSeconds`` (30 s).
/// Tokens are considered expired when `now + skewSeconds >= expiresAt`.
/// - Returns: `false` if `expiresAt` is `nil` (no expiry).
public func isExpired(now: Date = Date(), skewSeconds: TimeInterval = OAuthTokenExpirySkew.defaultSeconds) -> Bool {
guard let expiresAt else { return false }
return now.addingTimeInterval(skewSeconds) >= expiresAt
}
}
/// Token introspection result passed from the caller's token validator to ``BearerTokenValidator``.
///
/// The validator uses this to enforce expiry and audience checks before allowing a request through.
/// Produce an instance in your ``BearerTokenValidator/TokenValidator`` closure after verifying
/// the token's signature and extracting its claims.
public struct BearerTokenInfo: Sendable, Equatable {
/// Audience values from the token (`aud` JWT claim or introspection response).
///
/// `nil` indicates an opaque token whose audience cannot be inspected;
/// ``BearerTokenValidator`` skips the audience check in that case.
public let audience: [String]?
/// Scopes granted by the token.
public let scopes: Set<String>?
/// UTC date after which the token is considered expired.
///
/// `nil` means no expiry information is available and the expiry check is skipped.
public let expiresAt: Date?
public init(
audience: [String]? = nil,
scopes: Set<String>? = nil,
expiresAt: Date? = nil
) {
self.audience = audience
self.scopes = scopes
self.expiresAt = expiresAt
}
}
struct OAuthClientRegistrationResponse: Decodable {
let clientID: String
let clientSecret: String?
let tokenEndpointAuthMethod: String?
/// Unix timestamp after which `clientSecret` is no longer valid, per RFC 7591 §3.2.
/// A value of `0` means the secret does not expire.
let clientSecretExpiresAt: Int?
enum CodingKeys: String, CodingKey {
case clientID = "client_id"
case clientSecret = "client_secret"
case tokenEndpointAuthMethod = "token_endpoint_auth_method"
case clientSecretExpiresAt = "client_secret_expires_at"
}
}
/// Server-side encodable RFC 9728 Protected Resource Metadata.
///
/// Use this type to construct the metadata document that MCP servers **MUST** serve
/// at `/.well-known/oauth-protected-resource` per the MCP authorization specification.
///
/// Pair with ``ProtectedResourceMetadataValidator`` to automatically serve this
/// document in the server's validation pipeline.
public struct OAuthProtectedResourceServerMetadata: Codable, Sendable {
/// The canonical resource identifier (RFC 8707).
public let resource: String
/// One or more authorization server URLs that protect this resource.
public let authorizationServers: [URL]
/// The scopes supported by this resource server.
public let scopesSupported: [String]?
enum CodingKeys: String, CodingKey {
case resource
case authorizationServers = "authorization_servers"
case scopesSupported = "scopes_supported"
}
public init(
resource: String,
authorizationServers: [URL],
scopesSupported: [String]? = nil
) {
self.resource = resource
self.authorizationServers = authorizationServers
self.scopesSupported = scopesSupported
}
}