Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions Sources/OAuthenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public enum AuthenticatorError: Error, Hashable {
case refreshUnsupported
case refreshNotPossible
case tokenInvalid
case invalidRequest(String, String)
case invalidGrant(String, String)
case unrecognizedError(String, String)
case manualAuthenticationRequired
case httpResponseExpected
case unauthorizedRefreshFailed
Expand All @@ -26,6 +29,7 @@ public enum AuthenticatorError: Error, Hashable {
case stateTokenMismatch(String, String)
case issuingServerMismatch(String, String)
case pkceRequired
case rateLimited(HTTPURLResponse)
}

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

request.httpBody = Data(body.utf8)

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

return try JSONDecoder().decode(PARResponse.self, from: parData)
guard let httpResponse = response as? HTTPURLResponse else {
throw AuthenticatorError.httpResponseExpected
}

switch httpResponse.statusCode {
case 201:
return try JSONDecoder().decode(PARResponse.self, from: data)
// Expected response error status codes 405, 413, 429:
// See: https://www.rfc-editor.org/rfc/rfc9126.html#section-2.3
case 413:
throw AuthenticatorError.invalidRequest("invalid_request", "PAR Request body too large")
case 429:
throw AuthenticatorError.rateLimited(httpResponse)
default:
if let error = try? JSONDecoder().decode(OAuthErrorResponse.self, from: data) {
switch error.error {
case "invalid_request":
throw AuthenticatorError.invalidRequest(error.error, error.errorDescription ?? "")
default:
throw AuthenticatorError.unrecognizedError(error.error, error.errorDescription ?? "")
}
} else {
throw AuthenticatorError.unrecognizedError(
"unknown", "An unknown error occurred when making pushed authorization request")
}
}
}

private func getPARRequestURI() async throws -> String? {
Expand Down
126 changes: 70 additions & 56 deletions Sources/OAuthenticator/Services/Bluesky.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking
import FoundationNetworking
#endif

/// Find the spec here: https://atproto.com/specs/oauth
Expand Down Expand Up @@ -118,10 +119,15 @@ public enum Bluesky {
}
}

private static func loginProvider(server: ServerMetadata, validator: @escaping TokenSubscriberValidator) -> TokenHandling.LoginProvider {
private static func loginProvider(
server: ServerMetadata, validator: @escaping TokenSubscriberValidator
) -> TokenHandling.LoginProvider {
return { params in
// decode the params in the redirectURL
guard let redirectComponents = URLComponents(url: params.redirectURL, resolvingAgainstBaseURL: false) else {
guard
let redirectComponents = URLComponents(
url: params.redirectURL, resolvingAgainstBaseURL: false)
else {
throw AuthenticatorError.missingTokenURL
}

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

// and use them (plus just a little more) to construct the token request
guard let tokenURL = URL(string: server.tokenEndpoint) else {
throw AuthenticatorError.missingTokenURL
}

guard let verifier = params.pcke?.verifier else {
throw AuthenticatorError.pkceRequired
}
Expand All @@ -158,76 +159,89 @@ public enum Bluesky {
client_id: params.credentials.clientId
)

var request = URLRequest(url: tokenURL)

request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpBody = try JSONEncoder().encode(tokenRequest)

let (data, _) = try await params.responseProvider(request)

let tokenResponse = try JSONDecoder().decode(TokenResponse.self, from: data)

guard tokenResponse.token_type == "DPoP" else {
throw AuthenticatorError.dpopTokenExpected(tokenResponse.token_type)
}

if try await validator(tokenResponse, server.issuer) == false {
throw AuthenticatorError.tokenInvalid
}

return tokenResponse.login(for: iss)
return try await Bluesky.requestToken(
tokenRequest,
authorizationServer: server,
validator: validator,
responseProvider: params.responseProvider
)
}
}

private static func refreshProvider(server: ServerMetadata, validator: @escaping TokenSubscriberValidator) -> TokenHandling.RefreshProvider {
private static func refreshProvider(
server: ServerMetadata, validator: @escaping TokenSubscriberValidator
) -> TokenHandling.RefreshProvider {
{ login, credentials, responseProvider -> Login in
guard let refreshToken = login.refreshToken?.value else {
throw AuthenticatorError.refreshNotPossible
}

guard let tokenURL = URL(string: server.tokenEndpoint) else {
throw AuthenticatorError.missingTokenURL
}

let tokenRequest = RefreshTokenRequest(
refresh_token: refreshToken,
redirect_uri: credentials.callbackURL.absoluteString,
grant_type: "refresh_token",
client_id: credentials.clientId
)

var request = URLRequest(url: tokenURL)

request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(tokenRequest)

let (data, response) = try await responseProvider(request)
return try await Bluesky.requestToken(
tokenRequest,
authorizationServer: server,
validator: validator,
responseProvider: responseProvider
)
}
}

// make sure that we got a successful HTTP response
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 200 && httpResponse.statusCode < 300
else {
print("data:", String(decoding: data, as: UTF8.self))
print("response:", response)
private static func requestToken(
_ tokenRequest: Encodable,
authorizationServer: ServerMetadata,
validator: @escaping TokenSubscriberValidator,
responseProvider: URLResponseProvider
) async throws -> Login {
guard let tokenURL = URL(string: authorizationServer.tokenEndpoint) else {
throw AuthenticatorError.missingTokenURL
}

throw AuthenticatorError.refreshNotPossible
var request = URLRequest(url: tokenURL)

request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpBody = try JSONEncoder().encode(tokenRequest)

let (data, response) = try await responseProvider(request)

guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 200 && httpResponse.statusCode < 300
else {
if let error = try? JSONDecoder().decode(OAuthErrorResponse.self, from: data) {
switch error.error {
case "invalid_request":
throw AuthenticatorError.invalidRequest(error.error, error.errorDescription ?? "")
case "invalid_grant":
throw AuthenticatorError.invalidGrant(error.error, error.errorDescription ?? "")
default:
throw AuthenticatorError.unrecognizedError(error.error, error.errorDescription ?? "")
}
}

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

guard tokenResponse.token_type == "DPoP" else {
throw AuthenticatorError.dpopTokenExpected(tokenResponse.token_type)
}
guard let tokenResponse = try? JSONDecoder().decode(TokenResponse.self, from: data) else {
throw AuthenticatorError.unrecognizedError("invalid_json", "Decoding response JSON")
}

if try await validator(tokenResponse, server.issuer) == false {
throw AuthenticatorError.tokenInvalid
}
guard tokenResponse.token_type == "DPoP" else {
throw AuthenticatorError.dpopTokenExpected(tokenResponse.token_type)
}

return tokenResponse.login(for: server.issuer)
if try await validator(tokenResponse, authorizationServer.issuer) == false {
throw AuthenticatorError.tokenInvalid
}

return tokenResponse.login(for: authorizationServer.issuer)
}
}
Loading