Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Authenticator.response(for:) to return a generic data type #22

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 27 additions & 18 deletions Sources/OAuthenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum AuthenticatorError: Error {
}

/// Manage state required to executed authenticated URLRequests.
public actor Authenticator {
public actor Authenticator<UserDataType: Sendable> {
public typealias UserAuthenticator = @Sendable (URL, String) async throws -> URL
public typealias AuthenticationStatusHandler = (Result<Login, AuthenticatorError>) -> Void

Expand Down Expand Up @@ -85,32 +85,32 @@ public actor Authenticator {

let config: Configuration

let urlLoader: URLResponseProvider
let responseLoader: URLResponseProvider
let userDataLoader: URLUserDataProvider<UserDataType>
private var activeTokenTask: Task<Login, Error>?
private var localLogin: Login?

public init(config: Configuration, urlLoader loader: URLResponseProvider? = nil) {
public init(config: Configuration, responseLoader: URLResponseProvider? = nil, userDataLoader: @escaping URLUserDataProvider<UserDataType>) {
self.config = config

self.urlLoader = loader ?? URLSession.defaultProvider
self.responseLoader = responseLoader ?? URLSession.defaultProvider
self.userDataLoader = userDataLoader
}

/// A default `URLSession`-backed `URLResponseProvider`.
@available(*, deprecated, message: "Please move to URLSession.defaultProvider")
@MainActor
public static let defaultResponseProvider: URLResponseProvider = {
let session = URLSession(configuration: .default)
public init(config: Configuration, urlLoader: URLResponseProvider? = nil) where UserDataType == Data {
self.config = config

return session.responseProvider
}()
self.responseLoader = urlLoader ?? URLSession.defaultProvider
self.userDataLoader = urlLoader ?? URLSession.defaultProvider
}

/// Add authentication for `request`, execute it, and return its result.
public func response(for request: URLRequest) async throws -> (Data, URLResponse) {
public func response(for request: URLRequest) async throws -> (UserDataType, URLResponse) {
let userAuthenticator = config.userAuthenticator

let login = try await loginTaskResult(manual: false, userAuthenticator: userAuthenticator)

let result = try await authedResponse(for: request, login: login)
let result: (UserDataType, URLResponse) = try await authedResponse(for: request, login: login)

let action = try config.tokenHandling.responseStatusProvider(result)

Expand Down Expand Up @@ -146,13 +146,13 @@ public actor Authenticator {
}
}

private func authedResponse(for request: URLRequest, login: Login) async throws -> (Data, URLResponse) {
private func authedResponse(for request: URLRequest, login: Login) async throws -> (UserDataType, URLResponse) {
var authedRequest = request
let token = login.accessToken.value

authedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

return try await urlLoader(authedRequest)
return try await userDataLoader(authedRequest)
}

/// Manually perform user authentication, if required.
Expand All @@ -161,6 +161,15 @@ public actor Authenticator {
}
}

/// A default `URLSession`-backed `URLResponseProvider`.
@available(*, deprecated, message: "Please move to URLSession.defaultProvider")
@MainActor
public let defaultAuthenticatorResponseProvider: URLResponseProvider = {
let session = URLSession(configuration: .default)

return session.responseProvider
}()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this moved from Authentication.defaultResponseProvider?

Copy link
Author

@fritzt0 fritzt0 Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, yes. I would be happy to hear of a way to avoid this. Currently, Swift does not support statically stored properties in generic types. I know this would be source breaking. Perhaps using a computed, non-static property could mitigate this problem now that you have also deprecated its use? (But this would require an instance of Authentication.)


extension Authenticator {
private func retrieveLogin() async throws -> Login? {
guard let storage = config.loginStorage else {
Expand Down Expand Up @@ -256,7 +265,7 @@ extension Authenticator {
let scheme = try config.appCredentials.callbackURLScheme

let url = try await userAuthenticator(codeURL, scheme)
let login = try await config.tokenHandling.loginProvider(url, config.appCredentials, codeURL, urlLoader)
let login = try await config.tokenHandling.loginProvider(url, config.appCredentials, codeURL, responseLoader)

try await storeLogin(login)

Expand All @@ -276,7 +285,7 @@ extension Authenticator {
return nil
}

let login = try await refreshProvider(login, config.appCredentials, urlLoader)
let login = try await refreshProvider(login, config.appCredentials, responseLoader)

try await storeLogin(login)

Expand All @@ -285,7 +294,7 @@ extension Authenticator {
}

extension Authenticator {
public nonisolated var responseProvider: URLResponseProvider {
public nonisolated var responseProvider: URLUserDataProvider<UserDataType> {
{ try await self.response(for: $0) }
}
}
7 changes: 4 additions & 3 deletions Sources/OAuthenticator/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Foundation
/// This is used to abstract the actual networking system from the underlying authentication
/// mechanism.
public typealias URLResponseProvider = @Sendable (URLRequest) async throws -> (Data, URLResponse)
public typealias URLUserDataProvider<UserDataType: Sendable> = @Sendable (URLRequest) async throws -> (UserDataType, URLResponse)

public struct Token: Codable, Hashable, Sendable {
public let value: String
Expand Down Expand Up @@ -97,7 +98,7 @@ public struct TokenHandling {
public typealias AuthorizationURLProvider = @Sendable (AppCredentials) throws -> URL
public typealias LoginProvider = @Sendable (URL, AppCredentials, URL, URLResponseProvider) async throws -> Login
public typealias RefreshProvider = @Sendable (Login, AppCredentials, URLResponseProvider) async throws -> Login
public typealias ResponseStatusProvider = @Sendable ((Data, URLResponse)) throws -> ResponseStatus
public typealias ResponseStatusProvider = @Sendable ((any Sendable, URLResponse)) throws -> ResponseStatus

public let authorizationURLProvider: AuthorizationURLProvider
public let loginProvider: LoginProvider
Expand All @@ -115,12 +116,12 @@ public struct TokenHandling {
}

@Sendable
public static func allResponsesValid(result: (Data, URLResponse)) throws -> ResponseStatus {
public static func allResponsesValid<UserDataType: Sendable>(result: (UserDataType, URLResponse)) throws -> ResponseStatus {
return .valid
}

@Sendable
public static func refreshOrAuthorizeWhenUnauthorized(result: (Data, URLResponse)) throws -> ResponseStatus {
public static func refreshOrAuthorizeWhenUnauthorized<UserDataType: Sendable>(result: (UserDataType, URLResponse)) throws -> ResponseStatus {
guard let response = result.1 as? HTTPURLResponse else {
throw AuthenticatorError.httpResponseExpected
}
Expand Down
62 changes: 31 additions & 31 deletions Tests/OAuthenticatorTests/AuthenticatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
storeTokenExp.fulfill()
}

let config = Authenticator.Configuration(
let config = Authenticator<Data>.Configuration(
appCredentials: Self.mockCredentials,
loginStorage: storage,
// loginStorage: nil,
Expand All @@ -117,11 +117,11 @@
userAuthenticator: mockUserAuthenticator
)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 120 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 120 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 120 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

let (_, _) = try await auth.response(for: URLRequest(url: URL(string: "https://example.com")!))

await fulfillment(of: [retrieveTokenExp, userAuthExp, storeTokenExp, authedLoadExp], timeout: 1.0, enforceOrder: true)

Check warning on line 124 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races

Check warning on line 124 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races

Check warning on line 124 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races
}

@MainActor
Expand All @@ -148,16 +148,16 @@
XCTFail()
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 156 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 156 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 156 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

let (_, _) = try await auth.response(for: URLRequest(url: URL(string: "https://example.com")!))

await fulfillment(of: [retrieveTokenExp, authedLoadExp], timeout: 1.0, enforceOrder: true)

Check warning on line 160 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races

Check warning on line 160 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races

Check warning on line 160 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races
}

@MainActor
Expand Down Expand Up @@ -200,16 +200,16 @@
XCTAssertEqual(login.accessToken.value, "REFRESHED")
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 208 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 208 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 208 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

let (_, _) = try await auth.response(for: URLRequest(url: URL(string: "https://example.com")!))

await fulfillment(of: [retrieveTokenExp, refreshExp, storeTokenExp, authedLoadExp], timeout: 1.0, enforceOrder: true)

Check warning on line 212 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races

Check warning on line 212 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races

Check warning on line 212 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races
}

@MainActor
Expand All @@ -235,10 +235,10 @@
return URL(string: "my://login")!
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator)

let loadExp = expectation(description: "load url")
let mockLoader: URLResponseProvider = { request in
Expand All @@ -247,7 +247,7 @@
return ("hello".data(using: .utf8)!, URLResponse())
}

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 250 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 250 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 250 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

do {
let (_, _) = try await auth.response(for: URLRequest(url: URL(string: "https://example.com")!))
Expand All @@ -264,7 +264,7 @@

let (_, _) = try await auth.response(for: URLRequest(url: URL(string: "https://example.com")!))

await fulfillment(of: [userAuthExp, loadExp], timeout: 1.0, enforceOrder: true)

Check warning on line 267 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'XCTestCase' outside of main actor-isolated context may introduce data races
}

func testManualAuthenticationWithSuccessResult() async throws {
Expand Down Expand Up @@ -301,11 +301,11 @@
}

// Configure Authenticator with result callback
let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator,
authenticationStatusHandler: authenticationCallback)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator,
authenticationStatusHandler: authenticationCallback)

let loadExp = expectation(description: "load url")
let mockLoader: URLResponseProvider = { request in
Expand All @@ -314,7 +314,7 @@
return ("hello".data(using: .utf8)!, URLResponse())
}

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 317 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races
// Explicitly authenticate and grab Login information after
try await auth.authenticate()

Expand Down Expand Up @@ -357,11 +357,11 @@
}

// Configure Authenticator with result callback
let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: Authenticator.failingUserAuthenticator,
authenticationStatusHandler: authenticationCallback)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: Authenticator<Data>.failingUserAuthenticator,
authenticationStatusHandler: authenticationCallback)

let auth = Authenticator(config: config, urlLoader: nil)
do {
Expand Down Expand Up @@ -408,10 +408,10 @@
XCTAssertEqual(login.accessToken.value, "REFRESHED")
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader.responseProvider)

Expand Down Expand Up @@ -468,10 +468,10 @@
savedLogins.append(login)
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Expand Down
8 changes: 4 additions & 4 deletions Tests/OAuthenticatorTests/GoogleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ final class GoogleTests: XCTestCase {

let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!)
let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters)
let config = Authenticator.Configuration(
let config = Authenticator<Data>.Configuration(
appCredentials: creds,
tokenHandling: tokenHandling,
userAuthenticator: Authenticator.failingUserAuthenticator
userAuthenticator: Authenticator<Data>.failingUserAuthenticator
)

// Validate URL is properly constructed
Expand Down Expand Up @@ -76,10 +76,10 @@ final class GoogleTests: XCTestCase {

let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!)
let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters)
let config = Authenticator.Configuration(
let config = Authenticator<Data>.Configuration(
appCredentials: creds,
tokenHandling: tokenHandling,
userAuthenticator: Authenticator.failingUserAuthenticator
userAuthenticator: Authenticator<Data>.failingUserAuthenticator
)

// Validate URL is properly constructed
Expand Down
Loading