+4
Sources/CoreATProtocol/APEnvironment.swift
+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
+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
+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
+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
-1
Sources/CoreATProtocol/Networking/Services/NetworkingProtocol.swift