this repo has no description

oauth updates

Changed files
+186 -2
Sources
+4
Sources/CoreATProtocol/APEnvironment.swift
··· 5 // Created by Thomas Rademaker on 10/10/25. 6 // 7 8 @APActor 9 public class APEnvironment { 10 public static var current: APEnvironment = APEnvironment() ··· 14 public var refreshToken: String? 15 public var atProtocoldelegate: CoreATProtocolDelegate? 16 public var tokenRefreshHandler: (@Sendable () async throws -> Bool)? 17 public let routerDelegate = APRouterDelegate() 18 19 private init() {}
··· 5 // Created by Thomas Rademaker on 10/10/25. 6 // 7 8 + import JWTKit 9 + 10 @APActor 11 public class APEnvironment { 12 public static var current: APEnvironment = APEnvironment() ··· 16 public var refreshToken: String? 17 public var atProtocoldelegate: CoreATProtocolDelegate? 18 public var tokenRefreshHandler: (@Sendable () async throws -> Bool)? 19 + public var dpopPrivateKey: ES256PrivateKey? 20 + public var dpopKeys: JWTKeyCollection? 21 public let routerDelegate = APRouterDelegate() 22 23 private init() {}
+18
Sources/CoreATProtocol/CoreATProtocol.swift
··· 1 // The Swift Programming Language 2 // https://docs.swift.org/swift-book 3 4 // MARK: - Session 5 6 /// Represents an authenticated AT Protocol session ··· 48 @APActor 49 public func setTokenRefreshHandler(_ handler: (@Sendable () async throws -> Bool)?) { 50 APEnvironment.current.tokenRefreshHandler = handler 51 } 52 53 @APActor
··· 1 // The Swift Programming Language 2 // https://docs.swift.org/swift-book 3 4 + import JWTKit 5 + 6 // MARK: - Session 7 8 /// Represents an authenticated AT Protocol session ··· 50 @APActor 51 public func setTokenRefreshHandler(_ handler: (@Sendable () async throws -> Bool)?) { 52 APEnvironment.current.tokenRefreshHandler = handler 53 + } 54 + 55 + @APActor 56 + public func setDPoPPrivateKey(pem: String?) async throws { 57 + guard let pem, !pem.isEmpty else { 58 + APEnvironment.current.dpopPrivateKey = nil 59 + APEnvironment.current.dpopKeys = nil 60 + return 61 + } 62 + 63 + let privateKey = try ES256PrivateKey(pem: pem) 64 + let keys = JWTKeyCollection() 65 + await keys.add(ecdsa: privateKey) 66 + 67 + APEnvironment.current.dpopPrivateKey = privateKey 68 + APEnvironment.current.dpopKeys = keys 69 } 70 71 @APActor
+4
Sources/CoreATProtocol/Networking.swift
··· 36 private var refreshTask: Task<Bool, Error>? 37 38 public func intercept(_ request: inout URLRequest) async { 39 if let refreshToken = APEnvironment.current.refreshToken, shouldRefreshToken { 40 shouldRefreshToken = false 41 request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization")
··· 36 private var refreshTask: Task<Bool, Error>? 37 38 public func intercept(_ request: inout URLRequest) async { 39 + if APEnvironment.current.dpopPrivateKey != nil { 40 + return 41 + } 42 + 43 if let refreshToken = APEnvironment.current.refreshToken, shouldRefreshToken { 44 shouldRefreshToken = false 45 request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization")
+159 -1
Sources/CoreATProtocol/Networking/Services/NetworkRouter.swift
··· 1 import Foundation 2 3 @APActor 4 public protocol NetworkRouterDelegate: AnyObject { ··· 36 let networking: Networking 37 let urlSessionTaskDelegate: URLSessionTaskDelegate? 38 var decoder: JSONDecoder 39 40 public init(networking: Networking? = nil, urlSessionDelegate: URLSessionDelegate? = nil, urlSessionTaskDelegate: URLSessionTaskDelegate? = nil, decoder: JSONDecoder? = nil) { 41 if let networking = networking { ··· 61 guard var request = try? await buildRequest(from: route) else { throw NetworkError.encodingFailed } 62 await delegate?.intercept(&request) 63 64 - let (data, response) = try await networking.data(for: request, delegate: urlSessionTaskDelegate) 65 guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.noStatusCode } 66 switch httpResponse.statusCode { 67 case 200...299: ··· 85 return try await execute(route, attempts: attempts + 1) 86 } 87 } 88 89 func buildRequest(from route: Endpoint) async throws -> URLRequest { 90 ··· 119 } 120 } 121 }
··· 1 import Foundation 2 + import JWTKit 3 + import OAuthenticator 4 + #if canImport(CryptoKit) 5 + import CryptoKit 6 + #else 7 + import Crypto 8 + #endif 9 10 @APActor 11 public protocol NetworkRouterDelegate: AnyObject { ··· 43 let networking: Networking 44 let urlSessionTaskDelegate: URLSessionTaskDelegate? 45 var decoder: JSONDecoder 46 + private let dpopActor = DPoPRequestActor() 47 48 public init(networking: Networking? = nil, urlSessionDelegate: URLSessionDelegate? = nil, urlSessionTaskDelegate: URLSessionTaskDelegate? = nil, decoder: JSONDecoder? = nil) { 49 if let networking = networking { ··· 69 guard var request = try? await buildRequest(from: route) else { throw NetworkError.encodingFailed } 70 await delegate?.intercept(&request) 71 72 + let (data, response) = try await executeRequest(request) 73 guard let httpResponse = response as? HTTPURLResponse else { throw NetworkError.noStatusCode } 74 switch httpResponse.statusCode { 75 case 200...299: ··· 93 return try await execute(route, attempts: attempts + 1) 94 } 95 } 96 + 97 + private func executeRequest(_ request: URLRequest) async throws -> (Data, URLResponse) { 98 + if let accessToken = APEnvironment.current.accessToken, 99 + let privateKey = APEnvironment.current.dpopPrivateKey, 100 + let keys = APEnvironment.current.dpopKeys { 101 + return try await dpopResponse( 102 + for: request, 103 + accessToken: accessToken, 104 + privateKey: privateKey, 105 + keys: keys 106 + ) 107 + } 108 + 109 + return try await networking.data(for: request, delegate: urlSessionTaskDelegate) 110 + } 111 + 112 + private func dpopResponse( 113 + for request: URLRequest, 114 + accessToken: String, 115 + privateKey: ES256PrivateKey, 116 + keys: JWTKeyCollection 117 + ) async throws -> (Data, URLResponse) { 118 + let tokenHash = hashToken(accessToken) 119 + let jwtGenerator: DPoPSigner.JWTGenerator = { params in 120 + try await self.generateDPoPJWT( 121 + params: params, 122 + tokenHash: tokenHash, 123 + privateKey: privateKey, 124 + keys: keys 125 + ) 126 + } 127 + 128 + let responseProvider: URLResponseProvider = { request in 129 + try await self.networking.data(for: request, delegate: nil) 130 + } 131 + 132 + return try await dpopActor.response( 133 + request: request, 134 + jwtGenerator: jwtGenerator, 135 + token: accessToken, 136 + tokenHash: tokenHash, 137 + provider: responseProvider 138 + ) 139 + } 140 + 141 + private func generateDPoPJWT( 142 + params: DPoPSigner.JWTParameters, 143 + tokenHash: String, 144 + privateKey: ES256PrivateKey, 145 + keys: JWTKeyCollection 146 + ) async throws -> String { 147 + let htu = stripQueryAndFragment(from: params.requestEndpoint) 148 + let payload = DPoPRequestPayload( 149 + htm: params.httpMethod, 150 + htu: htu, 151 + iat: .init(value: .now), 152 + jti: .init(value: UUID().uuidString), 153 + nonce: params.nonce, 154 + ath: tokenHash 155 + ) 156 + 157 + var header = JWTHeader() 158 + header.typ = "dpop+jwt" 159 + header.alg = "ES256" 160 + 161 + if let keyParams = privateKey.parameters { 162 + let xBase64URL = keyParams.x 163 + .replacingOccurrences(of: "+", with: "-") 164 + .replacingOccurrences(of: "/", with: "_") 165 + .replacingOccurrences(of: "=", with: "") 166 + let yBase64URL = keyParams.y 167 + .replacingOccurrences(of: "+", with: "-") 168 + .replacingOccurrences(of: "/", with: "_") 169 + .replacingOccurrences(of: "=", with: "") 170 + 171 + header.jwk = [ 172 + "kty": .string("EC"), 173 + "crv": .string("P-256"), 174 + "x": .string(xBase64URL), 175 + "y": .string(yBase64URL) 176 + ] 177 + } 178 + 179 + return try await keys.sign(payload, header: header) 180 + } 181 + 182 + private func stripQueryAndFragment(from url: String) -> String { 183 + let fragmentIndex = url.firstIndex(of: "#").map { url.distance(from: url.startIndex, to: $0) } ?? -1 184 + let queryIndex = url.firstIndex(of: "?").map { url.distance(from: url.startIndex, to: $0) } ?? -1 185 + 186 + let end: Int 187 + if fragmentIndex == -1 { 188 + end = queryIndex 189 + } else if queryIndex == -1 { 190 + end = fragmentIndex 191 + } else { 192 + end = min(fragmentIndex, queryIndex) 193 + } 194 + 195 + return end == -1 ? url : String(url.prefix(end)) 196 + } 197 + 198 + private func hashToken(_ token: String) -> String { 199 + let digest = SHA256.hash(data: Data(token.utf8)) 200 + return Data(digest).base64URLEncodedString() 201 + } 202 203 func buildRequest(from route: Endpoint) async throws -> URLRequest { 204 ··· 233 } 234 } 235 } 236 + 237 + private struct DPoPRequestPayload: JWTPayload { 238 + let htm: String 239 + let htu: String 240 + let iat: IssuedAtClaim 241 + let jti: IDClaim 242 + let nonce: String? 243 + let ath: String? 244 + 245 + func verify(using key: some JWTAlgorithm) throws { 246 + // No additional verification needed for DPoP 247 + } 248 + } 249 + 250 + private actor DPoPRequestActor { 251 + private let signer = DPoPSigner() 252 + 253 + func response( 254 + request: URLRequest, 255 + jwtGenerator: DPoPSigner.JWTGenerator, 256 + token: String, 257 + tokenHash: String, 258 + provider: URLResponseProvider 259 + ) async throws -> (Data, URLResponse) { 260 + try await signer.response( 261 + isolation: self, 262 + for: request, 263 + using: jwtGenerator, 264 + token: token, 265 + tokenHash: tokenHash, 266 + issuingServer: nil, 267 + provider: provider 268 + ) 269 + } 270 + } 271 + 272 + private extension Data { 273 + func base64URLEncodedString() -> String { 274 + base64EncodedString() 275 + .replacingOccurrences(of: "+", with: "-") 276 + .replacingOccurrences(of: "/", with: "_") 277 + .replacingOccurrences(of: "=", with: "") 278 + } 279 + }
+1 -1
Sources/CoreATProtocol/Networking/Services/NetworkingProtocol.swift
··· 1 @preconcurrency import Foundation 2 3 @APActor 4 - public protocol Networking { 5 func data(for request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse) 6 } 7
··· 1 @preconcurrency import Foundation 2 3 @APActor 4 + public protocol Networking: Sendable { 5 func data(for request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse) 6 } 7