Skip to content

Commit 0962bcc

Browse files
authored
Fix token & PAR error handling (#54)
* Fix Bluesky token error handling This is actually generic OAuth token response logic, but the package is currently structured to make this bluesky specific. * Fix PAR error handling
1 parent 566de73 commit 0962bcc

File tree

2 files changed

+101
-58
lines changed

2 files changed

+101
-58
lines changed

Sources/OAuthenticator/Authenticator.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ public enum AuthenticatorError: Error, Hashable {
1414
case refreshUnsupported
1515
case refreshNotPossible
1616
case tokenInvalid
17+
case invalidRequest(String, String)
18+
case invalidGrant(String, String)
19+
case unrecognizedError(String, String)
1720
case manualAuthenticationRequired
1821
case httpResponseExpected
1922
case unauthorizedRefreshFailed
@@ -26,6 +29,7 @@ public enum AuthenticatorError: Error, Hashable {
2629
case stateTokenMismatch(String, String)
2730
case issuingServerMismatch(String, String)
2831
case pkceRequired
32+
case rateLimited(HTTPURLResponse)
2933
}
3034

3135
/// Manage state required to executed authenticated URLRequests.
@@ -395,9 +399,34 @@ extension Authenticator {
395399

396400
request.httpBody = Data(body.utf8)
397401

398-
let (parData, _) = try await self.dpopResponse(for: request, login: nil, isAuthServer: true)
402+
let (data, response) = try await self.dpopResponse(for: request, login: nil, isAuthServer: true)
399403

400-
return try JSONDecoder().decode(PARResponse.self, from: parData)
404+
guard let httpResponse = response as? HTTPURLResponse else {
405+
throw AuthenticatorError.httpResponseExpected
406+
}
407+
408+
switch httpResponse.statusCode {
409+
case 201:
410+
return try JSONDecoder().decode(PARResponse.self, from: data)
411+
// Expected response error status codes 405, 413, 429:
412+
// See: https://www.rfc-editor.org/rfc/rfc9126.html#section-2.3
413+
case 413:
414+
throw AuthenticatorError.invalidRequest("invalid_request", "PAR Request body too large")
415+
case 429:
416+
throw AuthenticatorError.rateLimited(httpResponse)
417+
default:
418+
if let error = try? JSONDecoder().decode(OAuthErrorResponse.self, from: data) {
419+
switch error.error {
420+
case "invalid_request":
421+
throw AuthenticatorError.invalidRequest(error.error, error.errorDescription ?? "")
422+
default:
423+
throw AuthenticatorError.unrecognizedError(error.error, error.errorDescription ?? "")
424+
}
425+
} else {
426+
throw AuthenticatorError.unrecognizedError(
427+
"unknown", "An unknown error occurred when making pushed authorization request")
428+
}
429+
}
401430
}
402431

403432
private func getPARRequestURI() async throws -> String? {

Sources/OAuthenticator/Services/Bluesky.swift

Lines changed: 70 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
2+
23
#if canImport(FoundationNetworking)
3-
import FoundationNetworking
4+
import FoundationNetworking
45
#endif
56

67
/// Find the spec here: https://atproto.com/specs/oauth
@@ -118,10 +119,15 @@ public enum Bluesky {
118119
}
119120
}
120121

121-
private static func loginProvider(server: ServerMetadata, validator: @escaping TokenSubscriberValidator) -> TokenHandling.LoginProvider {
122+
private static func loginProvider(
123+
server: ServerMetadata, validator: @escaping TokenSubscriberValidator
124+
) -> TokenHandling.LoginProvider {
122125
return { params in
123126
// decode the params in the redirectURL
124-
guard let redirectComponents = URLComponents(url: params.redirectURL, resolvingAgainstBaseURL: false) else {
127+
guard
128+
let redirectComponents = URLComponents(
129+
url: params.redirectURL, resolvingAgainstBaseURL: false)
130+
else {
125131
throw AuthenticatorError.missingTokenURL
126132
}
127133

@@ -141,11 +147,6 @@ public enum Bluesky {
141147
throw AuthenticatorError.issuingServerMismatch(iss, server.issuer)
142148
}
143149

144-
// and use them (plus just a little more) to construct the token request
145-
guard let tokenURL = URL(string: server.tokenEndpoint) else {
146-
throw AuthenticatorError.missingTokenURL
147-
}
148-
149150
guard let verifier = params.pcke?.verifier else {
150151
throw AuthenticatorError.pkceRequired
151152
}
@@ -158,76 +159,89 @@ public enum Bluesky {
158159
client_id: params.credentials.clientId
159160
)
160161

161-
var request = URLRequest(url: tokenURL)
162-
163-
request.httpMethod = "POST"
164-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
165-
request.setValue("application/json", forHTTPHeaderField: "Accept")
166-
request.httpBody = try JSONEncoder().encode(tokenRequest)
167-
168-
let (data, _) = try await params.responseProvider(request)
169-
170-
let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
171-
172-
guard tokenResponse.token_type == "DPoP" else {
173-
throw AuthenticatorError.dpopTokenExpected(tokenResponse.token_type)
174-
}
175-
176-
if try await validator(tokenResponse, server.issuer) == false {
177-
throw AuthenticatorError.tokenInvalid
178-
}
179-
180-
return tokenResponse.login(for: iss)
162+
return try await Bluesky.requestToken(
163+
tokenRequest,
164+
authorizationServer: server,
165+
validator: validator,
166+
responseProvider: params.responseProvider
167+
)
181168
}
182169
}
183170

184-
private static func refreshProvider(server: ServerMetadata, validator: @escaping TokenSubscriberValidator) -> TokenHandling.RefreshProvider {
171+
private static func refreshProvider(
172+
server: ServerMetadata, validator: @escaping TokenSubscriberValidator
173+
) -> TokenHandling.RefreshProvider {
185174
{ login, credentials, responseProvider -> Login in
186175
guard let refreshToken = login.refreshToken?.value else {
187176
throw AuthenticatorError.refreshNotPossible
188177
}
189178

190-
guard let tokenURL = URL(string: server.tokenEndpoint) else {
191-
throw AuthenticatorError.missingTokenURL
192-
}
193-
194179
let tokenRequest = RefreshTokenRequest(
195180
refresh_token: refreshToken,
196181
redirect_uri: credentials.callbackURL.absoluteString,
197182
grant_type: "refresh_token",
198183
client_id: credentials.clientId
199184
)
200185

201-
var request = URLRequest(url: tokenURL)
202-
203-
request.httpMethod = "POST"
204-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
205-
request.httpBody = try JSONEncoder().encode(tokenRequest)
206-
207-
let (data, response) = try await responseProvider(request)
186+
return try await Bluesky.requestToken(
187+
tokenRequest,
188+
authorizationServer: server,
189+
validator: validator,
190+
responseProvider: responseProvider
191+
)
192+
}
193+
}
208194

209-
// make sure that we got a successful HTTP response
210-
guard
211-
let httpResponse = response as? HTTPURLResponse,
212-
httpResponse.statusCode >= 200 && httpResponse.statusCode < 300
213-
else {
214-
print("data:", String(decoding: data, as: UTF8.self))
215-
print("response:", response)
195+
private static func requestToken(
196+
_ tokenRequest: Encodable,
197+
authorizationServer: ServerMetadata,
198+
validator: @escaping TokenSubscriberValidator,
199+
responseProvider: URLResponseProvider
200+
) async throws -> Login {
201+
guard let tokenURL = URL(string: authorizationServer.tokenEndpoint) else {
202+
throw AuthenticatorError.missingTokenURL
203+
}
216204

217-
throw AuthenticatorError.refreshNotPossible
205+
var request = URLRequest(url: tokenURL)
206+
207+
request.httpMethod = "POST"
208+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
209+
request.setValue("application/json", forHTTPHeaderField: "Accept")
210+
request.httpBody = try JSONEncoder().encode(tokenRequest)
211+
212+
let (data, response) = try await responseProvider(request)
213+
214+
guard
215+
let httpResponse = response as? HTTPURLResponse,
216+
httpResponse.statusCode >= 200 && httpResponse.statusCode < 300
217+
else {
218+
if let error = try? JSONDecoder().decode(OAuthErrorResponse.self, from: data) {
219+
switch error.error {
220+
case "invalid_request":
221+
throw AuthenticatorError.invalidRequest(error.error, error.errorDescription ?? "")
222+
case "invalid_grant":
223+
throw AuthenticatorError.invalidGrant(error.error, error.errorDescription ?? "")
224+
default:
225+
throw AuthenticatorError.unrecognizedError(error.error, error.errorDescription ?? "")
226+
}
218227
}
219228

220-
let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)
229+
throw AuthenticatorError.unrecognizedError(
230+
"unknown_response", "Received an unexpected response from the authorization server")
231+
}
221232

222-
guard tokenResponse.token_type == "DPoP" else {
223-
throw AuthenticatorError.dpopTokenExpected(tokenResponse.token_type)
224-
}
233+
guard let tokenResponse = try? JSONDecoder().decode(TokenResponse.self, from: data) else {
234+
throw AuthenticatorError.unrecognizedError("invalid_json", "Decoding response JSON")
235+
}
225236

226-
if try await validator(tokenResponse, server.issuer) == false {
227-
throw AuthenticatorError.tokenInvalid
228-
}
237+
guard tokenResponse.token_type == "DPoP" else {
238+
throw AuthenticatorError.dpopTokenExpected(tokenResponse.token_type)
239+
}
229240

230-
return tokenResponse.login(for: server.issuer)
241+
if try await validator(tokenResponse, authorizationServer.issuer) == false {
242+
throw AuthenticatorError.tokenInvalid
231243
}
244+
245+
return tokenResponse.login(for: authorizationServer.issuer)
232246
}
233247
}

0 commit comments

Comments
 (0)