From 7ad89282a7cb7e24c46ced6d56710ae0b93680b3 Mon Sep 17 00:00:00 2001 From: Thomas Rademaker Date: Fri, 17 Oct 2025 16:08:33 -0400 Subject: [PATCH] first pass at using libraries to implement oauth --- Package.swift | 10 ++- Sources/CoreATProtocol/LoginService.swift | 85 +++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 Sources/CoreATProtocol/LoginService.swift diff --git a/Package.swift b/Package.swift index a482d08..3153195 100644 --- a/Package.swift +++ b/Package.swift @@ -17,9 +17,17 @@ let package = Package( targets: ["CoreATProtocol"] ), ], + dependencies: [ + .package(url: "https://github.com/ChimeHQ/OAuthenticator", branch: "main"), + .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), + ], targets: [ .target( - name: "CoreATProtocol" + name: "CoreATProtocol", + dependencies: [ + "OAuthenticator", + .product(name: "JWTKit", package: "jwt-kit"), + ], ), .testTarget( name: "CoreATProtocolTests", diff --git a/Sources/CoreATProtocol/LoginService.swift b/Sources/CoreATProtocol/LoginService.swift new file mode 100644 index 0000000..5876b80 --- /dev/null +++ b/Sources/CoreATProtocol/LoginService.swift @@ -0,0 +1,85 @@ +// +// LoginService.swift +// CoreATProtocol +// +// Created by Thomas Rademaker on 10/17/25. +// + +import Foundation +import OAuthenticator +import JWTKit +import CryptoKit + +@APActor +class LoginService { + private var keys: JWTKeyCollection + private var privateKey: ES256PrivateKey + + public init() async { + // Create keys once during initialization + self.privateKey = ES256PrivateKey() + self.keys = JWTKeyCollection() + // Add the key to the collection + await self.keys.add(ecdsa: privateKey) + } + + public func login(account: String, clientMetadataEndpoint: String) async throws { + let provider = URLSession.defaultProvider + let host = APEnvironment.current.host ?? "" + let server = if host.hasPrefix("https://") { + String(host.dropFirst(8)) + } else if host.hasPrefix("http://") { + String(host.dropFirst(7)) + } else { host } + + let clientConfig = try await ClientMetadata.load(for: clientMetadataEndpoint, provider: provider) + let serverConfig = try await ServerMetadata.load(for: server, provider: provider) + + // Create storage for persisting login state + let loginStorage = LoginStorage { + // Implement retrieving stored login + // Return stored Login if it exists, or nil + return nil + } storeLogin: { login in + // Implement storing the login + // Store the login securely + + print("LOGIN: \(login)") + } + + let jwtGenerator: DPoPSigner.JWTGenerator = { params in + try await self.generateJWT(params: params) + } + + let tokenHandling = Bluesky.tokenHandling(account: account, server: serverConfig, jwtGenerator: jwtGenerator) + let config = Authenticator.Configuration(appCredentials: clientConfig.credentials, loginStorage: loginStorage, tokenHandling: tokenHandling, mode: .automatic) + let authenticator = Authenticator(config: config) + try await authenticator.authenticate() + } + + private func generateJWT(params: DPoPSigner.JWTParameters) async throws -> String { + // Create DPoP payload using existing keys + let payload = DPoPPayload( + htm: params.httpMethod, + htu: params.requestEndpoint, + iat: .init(value: .now), + jti: .init(value: UUID().uuidString), + nonce: params.nonce + ) + + // Sign with existing keys + return try await self.keys.sign(payload) + } +} + +private struct DPoPPayload: JWTPayload { + let htm: String + let htu: String + let iat: IssuedAtClaim + let jti: IDClaim + let nonce: String? + + func verify(using key: some JWTAlgorithm) throws { + // No additional verification needed + } +} -- 2.51.2