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

62 lines
2.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
#if canImport(CryptoKit)
import CryptoKit
#endif
/// Pure PKCE (RFC 7636) helpers required by the authorization_code flow.
public enum PKCE {
/// Generates a cryptographically random PKCE code verifier.
///
/// - Parameter length: Number of characters in the verifier. Defaults to 64.
/// RFC 7636 requires 43–128 characters.
/// - Returns: A URL-safe random string suitable for use as a PKCE code verifier.
public static func makeVerifier(length: Int = 64) -> String {
let charset = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
// 66 characters; 256 % 66 == 52, so reject bytes > 252 to eliminate modulo bias.
let limit = UInt8(255 - (255 % charset.count)) // 252
var rng = SystemRandomNumberGenerator()
var result = ""
result.reserveCapacity(length)
while result.count < length {
let byte = UInt8.random(in: 0...255, using: &rng)
if byte <= limit {
result.append(charset[Int(byte % UInt8(charset.count))])
}
}
return result
}
/// Derives the PKCE S256 code challenge from a verifier.
///
/// - Parameter verifier: A code verifier produced by ``makeVerifier(length:)``.
/// - Returns: The base64url-encoded SHA-256 hash of the verifier.
/// - Throws: ``OAuthAuthorizationError/pkceS256Unavailable`` on platforms without CryptoKit.
public static func makeChallenge(from verifier: String) throws -> String {
#if canImport(CryptoKit)
let hash = SHA256.hash(data: Data(verifier.utf8))
return Data(hash).base64URLEncodedString()
#else
throw OAuthAuthorizationError.pkceS256Unavailable
#endif
}
/// Verifies that the authorization server metadata advertises S256 PKCE support.
///
/// - Parameter metadata: Authorization server metadata to inspect.
/// - Throws: ``OAuthAuthorizationError/pkceCodeChallengeMethodsMissing`` if
/// `code_challenge_methods_supported` is absent or empty, or
/// ``OAuthAuthorizationError/pkceS256NotSupported(advertisedMethods:)`` if S256 is not listed.
static func checkSupport(in metadata: OAuthAuthorizationServerMetadata) throws {
guard let methods = metadata.codeChallengeMethodsSupported, !methods.isEmpty else {
throw OAuthAuthorizationError.pkceCodeChallengeMethodsMissing
}
let supportsS256 = methods.contains {
$0.caseInsensitiveCompare(OAuthCodeChallengeMethod.s256) == .orderedSame
}
guard supportsS256 else {
throw OAuthAuthorizationError.pkceS256NotSupported(advertisedMethods: methods)
}
}
}