* 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>
203 lines
10 KiB
Swift
203 lines
10 KiB
Swift
import Foundation
|
|
|
|
/// Errors thrown during OAuth 2.1 authorization by ``OAuthAuthorizer``.
|
|
///
|
|
/// These errors surface when the authorizer is unable to complete the authorization flow,
|
|
/// either due to discovery failures, token exchange problems, security policy violations,
|
|
/// or authorization code flow issues.
|
|
public enum OAuthAuthorizationError: LocalizedError {
|
|
/// No authorization server URL could be found in the Protected Resource Metadata.
|
|
case missingAuthorizationServer
|
|
|
|
/// All Protected Resource Metadata discovery candidates returned errors or invalid documents.
|
|
case metadataDiscoveryFailed
|
|
|
|
/// All Authorization Server Metadata discovery candidates (RFC 8414 / OIDC) returned errors.
|
|
case authorizationServerMetadataDiscoveryFailed
|
|
|
|
/// The authorization server metadata does not include a `token_endpoint`.
|
|
case tokenEndpointMissing
|
|
|
|
/// The token endpoint returned a non-2xx HTTP response.
|
|
///
|
|
/// - Parameters:
|
|
/// - statusCode: HTTP status code from the token endpoint.
|
|
/// - oauthError: The `error` field from the OAuth error response body, if present.
|
|
case tokenRequestFailed(statusCode: Int, oauthError: String?)
|
|
|
|
/// The token response body is missing `access_token`, has an empty token, or specifies
|
|
/// a non-Bearer token type.
|
|
case tokenResponseInvalid
|
|
|
|
/// A URL that must be a valid resource identifier (RFC 8707) failed validation.
|
|
case invalidResourceURI(String)
|
|
|
|
/// The configured client ID looks like a URL but is not a valid HTTPS URL with a path,
|
|
/// which is required for Client ID Metadata Documents.
|
|
case invalidClientIDMetadataURL(String)
|
|
|
|
/// The `resource` field in the Protected Resource Metadata does not match the requested endpoint.
|
|
///
|
|
/// - Parameters:
|
|
/// - expected: The canonical URI derived from the endpoint.
|
|
/// - actual: The canonical URI derived from the metadata's `resource` field.
|
|
case protectedResourceMismatch(expected: String, actual: String)
|
|
|
|
/// The authorization server does not support dynamic registration and no pre-registered
|
|
/// credentials were supplied.
|
|
case registrationInformationRequired
|
|
|
|
/// The authorization server does not support Client ID Metadata Documents and no dynamic
|
|
/// registration endpoint is available.
|
|
///
|
|
/// - Parameter clientID: The client ID URL that was provided as a CIMD URL.
|
|
case cimdNotSupported(clientID: String)
|
|
|
|
/// A URL received from a discovery response resolves to a private or reserved IP address,
|
|
/// which is blocked to prevent SSRF attacks.
|
|
///
|
|
/// - Parameters:
|
|
/// - context: Human-readable label identifying which URL was blocked.
|
|
/// - url: The blocked URL string.
|
|
case privateIPAddressBlocked(context: String, url: String)
|
|
|
|
/// An endpoint URL used during the OAuth flow does not satisfy the HTTPS-or-loopback requirement.
|
|
///
|
|
/// - Parameters:
|
|
/// - context: Human-readable label identifying which endpoint failed (e.g., `"Token endpoint"`).
|
|
/// - url: The offending URL string.
|
|
case insecureOAuthEndpoint(context: String, url: String)
|
|
|
|
/// An authorization server endpoint does not satisfy the HTTPS-only requirement.
|
|
///
|
|
/// - Parameters:
|
|
/// - context: Human-readable label identifying which endpoint failed.
|
|
/// - url: The offending URL string.
|
|
case insecureAuthorizationServerEndpoint(context: String, url: String)
|
|
|
|
/// The redirect URI supplied for the `authorization_code` flow is not a valid HTTPS or
|
|
/// loopback HTTP URI, or it contains a fragment.
|
|
case invalidRedirectURI(String)
|
|
|
|
/// The authorization endpoint returned an HTTP error response during the authorization code flow.
|
|
///
|
|
/// - Parameter statusCode: HTTP status code from the authorization endpoint.
|
|
case authorizationRequestFailed(statusCode: Int)
|
|
|
|
/// The authorization response did not include a `Location` redirect header.
|
|
case authorizationResponseMissingRedirectLocation
|
|
|
|
/// The redirect URI in the authorization response does not match the expected redirect URI.
|
|
///
|
|
/// - Parameters:
|
|
/// - expected: The redirect URI supplied in the authorization request.
|
|
/// - actual: The redirect URI received in the authorization response.
|
|
case authorizationResponseRedirectMismatch(expected: String, actual: String)
|
|
|
|
/// The authorization response redirect URL is missing the `state` parameter.
|
|
case authorizationResponseMissingState
|
|
|
|
/// The `state` in the authorization response does not match the one sent in the request.
|
|
///
|
|
/// This may indicate a CSRF attack.
|
|
///
|
|
/// - Parameters:
|
|
/// - expected: The `state` value sent in the authorization request.
|
|
/// - actual: The `state` value received in the authorization response.
|
|
case authorizationResponseStateMismatch(expected: String, actual: String)
|
|
|
|
/// The authorization response redirect URL is missing the `code` parameter.
|
|
case authorizationResponseMissingCode
|
|
|
|
/// The authorization server metadata does not include `code_challenge_methods_supported`,
|
|
/// which is required for PKCE (RFC 7636).
|
|
case pkceCodeChallengeMethodsMissing
|
|
|
|
/// The authorization server does not advertise `S256` in `code_challenge_methods_supported`.
|
|
///
|
|
/// The MCP specification mandates S256; plain PKCE is not accepted.
|
|
///
|
|
/// - Parameter advertisedMethods: The methods listed in the server metadata.
|
|
case pkceS256NotSupported(advertisedMethods: [String])
|
|
|
|
/// PKCE S256 challenge generation is unavailable because CryptoKit is not present on this platform.
|
|
case pkceS256Unavailable
|
|
|
|
/// The `issuer` field in the Authorization Server Metadata does not match the URL used to
|
|
/// resolve the metadata document, as required by RFC 8414 §3.
|
|
///
|
|
/// - Parameters:
|
|
/// - expected: The issuer URL derived from the discovery candidate.
|
|
/// - actual: The `issuer` field value found in the metadata document.
|
|
case authorizationServerIssuerMismatch(expected: String, actual: String)
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .missingAuthorizationServer:
|
|
return "No authorization server was found in protected resource metadata"
|
|
case .metadataDiscoveryFailed:
|
|
return "Failed to discover protected resource metadata"
|
|
case .authorizationServerMetadataDiscoveryFailed:
|
|
return "Failed to discover authorization server metadata"
|
|
case .tokenEndpointMissing:
|
|
return "Authorization server metadata is missing token_endpoint"
|
|
case .tokenRequestFailed(let statusCode, let oauthError):
|
|
if let oauthError, !oauthError.isEmpty {
|
|
return "Token request failed with status \(statusCode) (oauth_error: \(oauthError))"
|
|
}
|
|
return "Token request failed with status \(statusCode)"
|
|
case .tokenResponseInvalid:
|
|
return "Token response is invalid"
|
|
case .invalidResourceURI(let detail):
|
|
return "Invalid resource URI: \(detail)"
|
|
case .invalidClientIDMetadataURL(let value):
|
|
return
|
|
"Client ID metadata document URL must use https and include a path: \(value)"
|
|
case .protectedResourceMismatch(let expected, let actual):
|
|
return
|
|
"Protected resource metadata resource mismatch. Expected \(expected), got \(actual)"
|
|
case .registrationInformationRequired:
|
|
return
|
|
"No supported client registration mechanism was available; provide pre-registered client credentials"
|
|
case .cimdNotSupported(let clientID):
|
|
return
|
|
"Authorization server does not support Client ID Metadata Documents; configure pre-registered credentials or ensure the server advertises client_id_metadata_document_supported: \(clientID)"
|
|
case .privateIPAddressBlocked(let context, let url):
|
|
return
|
|
"\(context) resolves to a private or reserved IP address which is blocked for SSRF protection: \(url)"
|
|
case .insecureOAuthEndpoint(let context, let url):
|
|
return "\(context) must use https or loopback http: \(url)"
|
|
case .insecureAuthorizationServerEndpoint(let context, let url):
|
|
return "\(context) must use https: \(url)"
|
|
case .invalidRedirectURI(let url):
|
|
return
|
|
"Redirect URI must use https or loopback http and must not include fragments: \(url)"
|
|
case .authorizationRequestFailed(let statusCode):
|
|
return "Authorization request failed with status \(statusCode)"
|
|
case .authorizationResponseMissingRedirectLocation:
|
|
return "Authorization response is missing redirect location"
|
|
case .authorizationResponseRedirectMismatch(let expected, let actual):
|
|
return
|
|
"Authorization response redirect URI mismatch. Expected \(expected), got \(actual)"
|
|
case .authorizationResponseMissingState:
|
|
return "Authorization response is missing state"
|
|
case .authorizationResponseStateMismatch(let expected, let actual):
|
|
return "Authorization response state mismatch. Expected \(expected), got \(actual)"
|
|
case .authorizationResponseMissingCode:
|
|
return "Authorization response is missing the authorization code"
|
|
case .pkceCodeChallengeMethodsMissing:
|
|
return
|
|
"Authorization server metadata must include code_challenge_methods_supported for PKCE"
|
|
case .pkceS256NotSupported(let advertisedMethods):
|
|
let methods = advertisedMethods.joined(separator: ", ")
|
|
return
|
|
"Authorization server metadata must support PKCE S256 (advertised: \(methods))"
|
|
case .pkceS256Unavailable:
|
|
return
|
|
"PKCE S256 code challenge generation is unavailable on this platform"
|
|
case .authorizationServerIssuerMismatch(let expected, let actual):
|
|
return
|
|
"Authorization server issuer mismatch. Expected \(expected), got \(actual)"
|
|
}
|
|
}
|
|
}
|