this repo has no description

further oauth support

+1 -1
Package.resolved
··· 1 1 { 2 - "originHash" : "1139e0e1075c4de720978803490da5da789019e0504be736dd4f216da7eadad4", 2 + "originHash" : "2237e2c10a8d530dcbd1f9770efc8fcf2a9fc2ca2c63a19882551fea7ab9fe25", 3 3 "pins" : [ 4 4 { 5 5 "identity" : "jwt-kit",
+6 -1
Sources/CoreATProtocol/APEnvironment.swift
··· 5 5 // Created by Thomas Rademaker on 10/10/25. 6 6 // 7 7 8 + import OAuthenticator 9 + 8 10 @APActor 9 11 public class APEnvironment { 10 12 public static var current: APEnvironment = APEnvironment() ··· 12 14 public var host: String? 13 15 public var accessToken: String? 14 16 public var refreshToken: String? 17 + public var login: Login? 18 + public var dpopProofGenerator: DPoPSigner.JWTGenerator? 19 + public var resourceServerNonce: String? 15 20 public var atProtocoldelegate: CoreATProtocolDelegate? 16 21 public let routerDelegate = APRouterDelegate() 22 + public let resourceDPoPSigner = DPoPSigner() 17 23 18 24 private init() {} 19 25 ··· 23 29 // self.userAgent = userAgent 24 30 // } 25 31 } 26 -
+28
Sources/CoreATProtocol/CoreATProtocol.swift
··· 1 1 // The Swift Programming Language 2 2 // https://docs.swift.org/swift-book 3 3 4 + @_exported import OAuthenticator 5 + 4 6 public protocol CoreATProtocolDelegate: AnyObject {} 5 7 6 8 @APActor ··· 26 28 public func update(hostURL: String?) { 27 29 APEnvironment.current.host = hostURL 28 30 } 31 + 32 + @APActor 33 + public func applyAuthenticationContext(login: Login, generator: @escaping DPoPSigner.JWTGenerator, resourceNonce: String? = nil) { 34 + APEnvironment.current.login = login 35 + APEnvironment.current.accessToken = login.accessToken.value 36 + APEnvironment.current.refreshToken = login.refreshToken?.value 37 + APEnvironment.current.dpopProofGenerator = generator 38 + APEnvironment.current.resourceServerNonce = resourceNonce 39 + APEnvironment.current.resourceDPoPSigner.nonce = resourceNonce 40 + } 41 + 42 + @APActor 43 + public func clearAuthenticationContext() { 44 + APEnvironment.current.login = nil 45 + APEnvironment.current.dpopProofGenerator = nil 46 + APEnvironment.current.resourceServerNonce = nil 47 + APEnvironment.current.accessToken = nil 48 + APEnvironment.current.refreshToken = nil 49 + APEnvironment.current.resourceDPoPSigner.nonce = nil 50 + } 51 + 52 + @APActor 53 + public func updateResourceDPoPNonce(_ nonce: String?) { 54 + APEnvironment.current.resourceServerNonce = nonce 55 + APEnvironment.current.resourceDPoPSigner.nonce = nonce 56 + }
+83
Sources/CoreATProtocol/DPoPJWTGenerator.swift
··· 1 + import Foundation 2 + import JWTKit 3 + import OAuthenticator 4 + 5 + public enum DPoPKeyMaterialError: Error, Equatable { 6 + case publicKeyUnavailable 7 + case invalidCoordinate 8 + } 9 + 10 + public actor DPoPJWTGenerator { 11 + private let privateKey: ES256PrivateKey 12 + private let keys: JWTKeyCollection 13 + private let jwkHeader: [String: JWTHeaderField] 14 + 15 + public init(privateKey: ES256PrivateKey) async throws { 16 + self.privateKey = privateKey 17 + self.keys = JWTKeyCollection() 18 + self.jwkHeader = try Self.makeJWKHeader(from: privateKey) 19 + await self.keys.add(ecdsa: privateKey) 20 + } 21 + 22 + public func jwtGenerator() -> DPoPSigner.JWTGenerator { 23 + { params in 24 + try await self.makeJWT(for: params) 25 + } 26 + } 27 + 28 + public func makeJWT(for params: DPoPSigner.JWTParameters) async throws -> String { 29 + var header = JWTHeader() 30 + header.typ = params.keyType 31 + header.alg = header.alg ?? "ES256" 32 + header.jwk = jwkHeader 33 + 34 + let issuedAt = Date() 35 + let payload = DPoPPayload( 36 + htm: params.httpMethod, 37 + htu: params.requestEndpoint, 38 + iat: IssuedAtClaim(value: issuedAt), 39 + exp: ExpirationClaim(value: issuedAt.addingTimeInterval(60)), 40 + jti: IDClaim(value: UUID().uuidString), 41 + nonce: params.nonce, 42 + iss: params.issuingServer.map { IssuerClaim(value: $0) }, 43 + ath: params.tokenHash 44 + ) 45 + 46 + return try await keys.sign(payload, header: header) 47 + } 48 + 49 + private static func makeJWKHeader(from key: ES256PrivateKey) throws -> [String: JWTHeaderField] { 50 + guard let parameters = key.publicKey.parameters else { 51 + throw DPoPKeyMaterialError.publicKeyUnavailable 52 + } 53 + 54 + guard 55 + let xData = Data(base64Encoded: parameters.x), 56 + let yData = Data(base64Encoded: parameters.y) 57 + else { 58 + throw DPoPKeyMaterialError.invalidCoordinate 59 + } 60 + 61 + return [ 62 + "kty": .string("EC"), 63 + "crv": .string("P-256"), 64 + "x": .string(xData.base64URLEncodedString()), 65 + "y": .string(yData.base64URLEncodedString()) 66 + ] 67 + } 68 + } 69 + 70 + struct DPoPPayload: JWTPayload { 71 + let htm: String 72 + let htu: String 73 + let iat: IssuedAtClaim 74 + let exp: ExpirationClaim 75 + let jti: IDClaim 76 + let nonce: String? 77 + let iss: IssuerClaim? 78 + let ath: String? 79 + 80 + func verify(using key: some JWTAlgorithm) throws { 81 + try exp.verifyNotExpired(currentDate: Date()) 82 + } 83 + }
+11
Sources/CoreATProtocol/Extensions/Data+Base64URL.swift
··· 1 + import Foundation 2 + 3 + extension Data { 4 + /// Returns a URL-safe Base64 representation without padding. 5 + func base64URLEncodedString() -> String { 6 + base64EncodedString() 7 + .replacingOccurrences(of: "+", with: "-") 8 + .replacingOccurrences(of: "/", with: "_") 9 + .replacingOccurrences(of: "=", with: "") 10 + } 11 + }
+18 -55
Sources/CoreATProtocol/LoginService.swift
··· 7 7 8 8 import Foundation 9 9 import OAuthenticator 10 - import JWTKit 11 - import CryptoKit 12 10 13 11 @APActor 14 - class LoginService { 15 - private var keys: JWTKeyCollection 16 - private var privateKey: ES256PrivateKey 17 - 18 - public init() async { 19 - // Create keys once during initialization 20 - self.privateKey = ES256PrivateKey() 21 - self.keys = JWTKeyCollection() 22 - // Add the key to the collection 23 - await self.keys.add(ecdsa: privateKey) 12 + public final class LoginService { 13 + public enum Error: Swift.Error { 14 + case missingStoredLogin 15 + } 16 + 17 + private let loginStorage: LoginStorage 18 + private let jwtGenerator: DPoPSigner.JWTGenerator 19 + 20 + public init(jwtGenerator: @escaping DPoPSigner.JWTGenerator, loginStorage: LoginStorage) { 21 + self.jwtGenerator = jwtGenerator 22 + self.loginStorage = loginStorage 24 23 } 25 - 26 - public func login(account: String, clientMetadataEndpoint: String) async throws { 24 + 25 + public func login(account: String, clientMetadataEndpoint: String) async throws -> Login { 27 26 let provider = URLSession.defaultProvider 28 27 let host = APEnvironment.current.host ?? "" 29 28 let server = if host.hasPrefix("https://") { ··· 34 33 35 34 let clientConfig = try await ClientMetadata.load(for: clientMetadataEndpoint, provider: provider) 36 35 let serverConfig = try await ServerMetadata.load(for: server, provider: provider) 37 - 38 - // Create storage for persisting login state 39 - let loginStorage = LoginStorage { 40 - // Implement retrieving stored login 41 - // Return stored Login if it exists, or nil 42 - return nil 43 - } storeLogin: { login in 44 - // Implement storing the login 45 - // Store the login securely 46 - 47 - print("LOGIN: \(login)") 48 - } 49 - 50 - let jwtGenerator: DPoPSigner.JWTGenerator = { params in 51 - try await self.generateJWT(params: params) 52 - } 53 36 54 37 let tokenHandling = Bluesky.tokenHandling(account: account, server: serverConfig, jwtGenerator: jwtGenerator) 55 38 let config = Authenticator.Configuration(appCredentials: clientConfig.credentials, loginStorage: loginStorage, tokenHandling: tokenHandling, mode: .automatic) 56 39 let authenticator = Authenticator(config: config) 57 40 try await authenticator.authenticate() 58 - } 59 - 60 - private func generateJWT(params: DPoPSigner.JWTParameters) async throws -> String { 61 - // Create DPoP payload using existing keys 62 - let payload = DPoPPayload( 63 - htm: params.httpMethod, 64 - htu: params.requestEndpoint, 65 - iat: .init(value: .now), 66 - jti: .init(value: UUID().uuidString), 67 - nonce: params.nonce 68 - ) 69 - 70 - // Sign with existing keys 71 - return try await self.keys.sign(payload) 72 - } 73 - } 74 41 75 - private struct DPoPPayload: JWTPayload { 76 - let htm: String 77 - let htu: String 78 - let iat: IssuedAtClaim 79 - let jti: IDClaim 80 - let nonce: String? 81 - 82 - func verify(using key: some JWTAlgorithm) throws { 83 - // No additional verification needed 42 + guard let storedLogin = try await loginStorage.retrieveLogin() else { 43 + throw Error.missingStoredLogin 44 + } 45 + 46 + return storedLogin 84 47 } 85 48 }
+35 -4
Sources/CoreATProtocol/Networking.swift
··· 6 6 // 7 7 8 8 import Foundation 9 + import CryptoKit 10 + @preconcurrency import OAuthenticator 9 11 10 12 extension JSONDecoder { 11 13 public static var atDecoder: JSONDecoder { ··· 30 32 return differenceInMinutes >= timeLimit 31 33 } 32 34 33 - @APActor 35 + @MainActor 34 36 public class APRouterDelegate: NetworkRouterDelegate { 35 37 private var shouldRefreshToken = false 36 38 37 39 public func intercept(_ request: inout URLRequest) async { 38 - if let refreshToken = APEnvironment.current.refreshToken, shouldRefreshToken { 40 + if let generator = await APEnvironment.current.dpopProofGenerator, 41 + let login = await APEnvironment.current.login { 42 + let token = login.accessToken.value 43 + let tokenHash = tokenHash(for: token) 44 + let signer = await APEnvironment.current.resourceDPoPSigner 45 + signer.nonce = await APEnvironment.current.resourceServerNonce 46 + 47 + do { 48 + try await signer.authenticateRequest( 49 + &request, 50 + isolation: MainActor.shared, 51 + using: generator, 52 + token: token, 53 + tokenHash: tokenHash, 54 + issuer: login.issuingServer 55 + ) 56 + } catch { 57 + // If DPoP signing fails, fall back to providing the token directly. 58 + request.setValue("DPoP \(token)", forHTTPHeaderField: "Authorization") 59 + } 60 + 61 + return 62 + } 63 + 64 + if let refreshToken = await APEnvironment.current.refreshToken, shouldRefreshToken { 39 65 shouldRefreshToken = false 40 66 request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization") 41 - } else if let accessToken = APEnvironment.current.accessToken { 67 + } else if let accessToken = await APEnvironment.current.accessToken { 42 68 request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") 43 69 } 44 70 } ··· 65 91 message.error == AtErrorType.expiredToken.rawValue { 66 92 return try await getNewToken() 67 93 } 68 - 94 + 69 95 return false 96 + } 97 + 98 + private func tokenHash(for token: String) -> String { 99 + let digest = SHA256.hash(data: Data(token.utf8)) 100 + return Data(digest).base64URLEncodedString() 70 101 } 71 102 }
+1 -1
Sources/CoreATProtocol/Networking/Services/NetworkRouter.swift
··· 1 1 import Foundation 2 2 3 - @APActor 3 + @MainActor 4 4 public protocol NetworkRouterDelegate: AnyObject { 5 5 func intercept(_ request: inout URLRequest) async 6 6 func shouldRetry(error: Error, attempts: Int) async throws -> Bool