Skip to content

Commit

Permalink
Rename authentication result callback to use new name: Authentication…
Browse files Browse the repository at this point in the history
…StatusHandler

Update README.md to showcase new usage
Add new test to validate that error in AuthenticationStatusHandler is properly propagated
  • Loading branch information
martindufort committed Oct 2, 2023
1 parent 2ddc0ea commit 11ddb80
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 25 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,27 @@ let (data, response) = try await authenticator.response(for: myRequest)
```

If you want to receive the result of the authentication process without issuing a URLRequest first, you can specify
an optional `Authenticator.AuthenticationResult` callback function within the `Authenticator.Configuration` initializer.
an optional `Authenticator.AuthenticationStatusHandler` callback function within the `Authenticator.Configuration` initializer.

This allows you to support special cases where you need to capture the `Login` object before executing your first
authenticated URLRequest and manage that separately.
authenticated `URLRequest` and manage that separately.

``` swift
let authenticationResultCallback: Authenticator.AuthenticationResult = { login, error in
...
authenticatedLogin = login
let authenticationStatusHandler: Authenticator.AuthenticationStatusHandler = { result in
switch result {
case .success (let login):
authenticatedLogin = login
case .failure(let error):
print("Authentication failed: \(error)")
}
}

// Configure Authenticator with result callback
let config = Authenticator.Configuration(appCredentials: appCreds,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: userAuthenticator,
authenticationResult: authenticationResultCallback)
authenticationStatusHandler: authenticationStatusHandler)
let auth = Authenticator(config: config, urlLoader: mockLoader)
try await auth.authenticate()
if let authenticatedLogin = authenticatedLogin {
Expand Down
21 changes: 10 additions & 11 deletions Sources/OAuthenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public enum AuthenticatorError: Error {
/// Manage state required to executed authenticated URLRequests.
public final class Authenticator {
public typealias UserAuthenticator = (URL, String) async throws -> URL
public typealias AuthenticationResult = (Login?, AuthenticatorError?) -> Void
public typealias AuthenticationStatusHandler = (Result<Login, AuthenticatorError>) -> Void

/// A `UserAuthenticator` that always fails. Useful as a placeholder
/// for testing and for doing manual authentication with an external
Expand Down Expand Up @@ -48,34 +48,34 @@ public final class Authenticator {
public let mode: UserAuthenticationMode

// Specify an authenticationResult closure to obtain result and grantedScope
public let authenticationResult: AuthenticationResult?
public let authenticationStatusHandler: AuthenticationStatusHandler?

@available(tvOS 16.0, macCatalyst 13.0, *)
public init(appCredentials: AppCredentials,
loginStorage: LoginStorage? = nil,
tokenHandling: TokenHandling,
mode: UserAuthenticationMode = .automatic,
authenticationResult: AuthenticationResult? = nil) {
authenticationStatusHandler: AuthenticationStatusHandler? = nil) {
self.appCredentials = appCredentials
self.loginStorage = loginStorage
self.tokenHandling = tokenHandling
self.mode = mode
self.userAuthenticator = ASWebAuthenticationSession.userAuthenticator
self.authenticationResult = authenticationResult
self.authenticationStatusHandler = authenticationStatusHandler
}

public init(appCredentials: AppCredentials,
loginStorage: LoginStorage? = nil,
tokenHandling: TokenHandling,
mode: UserAuthenticationMode = .automatic,
userAuthenticator: @escaping UserAuthenticator,
authenticationResult: AuthenticationResult? = nil) {
authenticationStatusHandler: AuthenticationStatusHandler? = nil) {
self.appCredentials = appCredentials
self.loginStorage = loginStorage
self.tokenHandling = tokenHandling
self.mode = mode
self.userAuthenticator = userAuthenticator
self.authenticationResult = authenticationResult
self.authenticationStatusHandler = authenticationStatusHandler
}
}

Expand Down Expand Up @@ -197,22 +197,21 @@ extension Authenticator {
private func loginTaskResult(manual: Bool, userAuthenticator: @escaping UserAuthenticator) async throws -> Login {
let task = activeTokenTask ?? makeLoginTask(manual: manual, userAuthenticator: userAuthenticator)

var login: Login? = nil
var login: Login
do {
login = try await loginFromTask(task: task)

// Inform authenticationResult closure of new login information
self.config.authenticationResult?(login, nil)
self.config.authenticationStatusHandler?(.success(login))
}
catch let authenticatorError as AuthenticatorError {
// Capture error and inform authenticationResult closure about it
self.config.authenticationResult?(nil, authenticatorError)
self.config.authenticationStatusHandler?(.failure(authenticatorError))

// Rethrow error
throw authenticatorError
}

return login!
return login
}

private func loginFromTask(task: Task<Login, Error>) async throws -> Login {
Expand Down
72 changes: 64 additions & 8 deletions Tests/OAuthenticatorTests/AuthenticatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ final class AuthenticatorTests: XCTestCase {
}

@MainActor
func testManualAuthenticationWithResultCallback() async throws {
func testManualAuthenticationWithSuccessResult() async throws {
let urlProvider: TokenHandling.AuthorizationURLProvider = { creds in
return URL(string: "my://auth?client_id=\(creds.clientId)")!
}
Expand All @@ -281,20 +281,21 @@ final class AuthenticatorTests: XCTestCase {

// This is the callback to obtain authentication results
var authenticatedLogin: Login?
let authenticationResultCallback: Authenticator.AuthenticationResult = { login, error in
if error != nil {
authenticatedLogin = nil
return
let authenticationCallback: Authenticator.AuthenticationStatusHandler = { result in
switch result {
case .failure(_):
XCTFail()
case .success(let login):
authenticatedLogin = login
}
authenticatedLogin = login
}

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

let loadExp = expectation(description: "load url")
let mockLoader: URLResponseProvider = { request in
Expand All @@ -309,13 +310,68 @@ final class AuthenticatorTests: XCTestCase {

// Ensure our authenticatedLogin objet is available and contains the proper Token
XCTAssertNotNil(authenticatedLogin)
XCTAssertEqual(authenticatedLogin?.accessToken.value, "TOKEN")
XCTAssertEqual(authenticatedLogin!, Login(token:"TOKEN"))

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

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

// Test AuthenticationResultHandler with a failed UserAuthenticator
@MainActor
func testManualAuthenticationWithFailedResult() async throws {
let urlProvider: TokenHandling.AuthorizationURLProvider = { creds in
return URL(string: "my://auth?client_id=\(creds.clientId)")!
}

let loginProvider: TokenHandling.LoginProvider = { url, creds, tokenUrl, _ in
XCTAssertEqual(url, URL(string: "my://login")!)

return Login(token: "TOKEN")
}

let tokenHandling = TokenHandling(authorizationURLProvider: urlProvider,
loginProvider: loginProvider,
responseStatusProvider: TokenHandling.allResponsesValid)

// This is the callback to obtain authentication results
var authenticatedLogin: Login?
let failureAuth = expectation(description: "auth failure")
let authenticationCallback: Authenticator.AuthenticationStatusHandler = { result in
switch result {
case .failure(_):
failureAuth.fulfill()
authenticatedLogin = nil
case .success(_):
XCTFail()
}
}

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

let auth = Authenticator(config: config, urlLoader: nil)
do {
// Explicitly authenticate and grab Login information after
try await auth.authenticate()

// Ensure our authenticatedLogin objet is *not* available
XCTAssertNil(authenticatedLogin)
}
catch let error as AuthenticatorError {
XCTAssertEqual(error, AuthenticatorError.failingAuthenticatorUsed)
}
catch {
throw error
}

await compatFulfillment(of: [failureAuth], timeout: 1.0, enforceOrder: true)
}

@MainActor
func testUnauthorizedRequestRefreshes() async throws {
let requestedURL = URL(string: "https://example.com")!
Expand Down

0 comments on commit 11ddb80

Please sign in to comment.