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
77 changes: 67 additions & 10 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 @@ -308,20 +312,20 @@ extension Authenticator {
pcke: config.tokenHandling.pkce,
parRequestURI: parRequestURI,
stateToken: stateToken,
responseProvider: { try await self.dpopResponse(for: $0, login: nil) }
responseProvider: { try await self.dpopResponse(for: $0, login: nil, isAuthServer: true) }
)

let tokenURL = try await config.tokenHandling.authorizationURLProvider(authConfig)

let scheme = try config.appCredentials.callbackURLScheme

let callbackURL = try await userAuthenticator(tokenURL, scheme)
let callbackURL = try await userAuthenticator(tokenURL, scheme)

let params = TokenHandling.LoginProviderParameters(
authorizationURL: tokenURL,
credentials: config.appCredentials,
redirectURL: callbackURL,
responseProvider: { try await self.dpopResponse(for: $0, login: nil) },
responseProvider: { try await self.dpopResponse(for: $0, login: nil, isAuthServer: true) },
stateToken: stateToken,
pcke: config.tokenHandling.pkce
)
Expand All @@ -347,7 +351,11 @@ extension Authenticator {
}

do {
let login = try await refreshProvider(login, config.appCredentials, { try await self.dpopResponse(for: $0, login: nil) })
let login = try await refreshProvider(
login, config.appCredentials,
{
try await self.dpopResponse(for: $0, login: nil, isAuthServer: true)
})

try await storeLogin(login)

Expand All @@ -365,7 +373,7 @@ extension Authenticator {
}

let challenge = pkce.challenge
let scopes = config.appCredentials.scopes.joined(separator: " ")
let scopes = config.appCredentials.scopeString
let callbackURI = config.appCredentials.callbackURL
let clientId = config.appCredentials.clientId

Expand All @@ -391,9 +399,34 @@ extension Authenticator {

request.httpBody = Data(body.utf8)

let (parData, _) = try await dpopResponse(for: request, login: nil)
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 All @@ -412,7 +445,31 @@ extension Authenticator {
{ try await self.response(for: $0) }
}

private func dpopResponse(for request: URLRequest, login: Login?) async throws -> (Data, URLResponse) {
private func dpopResponse(for request: URLRequest, login: Login?) async throws -> (
Data, URLResponse
) {
var issuer: String? = nil
if let iss = login?.issuingServer {
issuer = URL(string: iss)?.origin
}

guard let requestOrigin = request.url?.origin else {
throw DPoPError.requestInvalid(request)
}

let isAuthServer = issuer == nil || issuer == requestOrigin

return try await dpopResponse(
for: request,
login: login,
isAuthServer: isAuthServer
)
}

private func dpopResponse(for request: URLRequest, login: Login?, isAuthServer: Bool?)
async throws -> (Data, URLResponse)
{
print("Request: \(request.httpMethod!) - \(request.url?.absoluteString ?? "missing url")")
guard let generator = config.tokenHandling.dpopJWTGenerator else {
return try await urlLoader(request)
}
Expand All @@ -430,8 +487,8 @@ extension Authenticator {
using: generator,
token: token,
tokenHash: tokenHash,
issuingServer: login?.issuingServer,
provider: urlLoader
isAuthServer: isAuthServer,
responseProvider: urlLoader
)
}
}
Loading
Loading