diff --git a/Sources/OAuthKit/OAuth+State.swift b/Sources/OAuthKit/OAuth+State.swift index cb3df55..7e079aa 100644 --- a/Sources/OAuthKit/OAuth+State.swift +++ b/Sources/OAuthKit/OAuth+State.swift @@ -43,5 +43,11 @@ extension OAuth { /// - Provider: the oauth provider /// - Authorization: the oauth authorization case authorized(Provider, Authorization) + + /// An error has occurred during an authorization flow for the specified provider. + /// - Parameters: + /// - Provider: the oauth provider + /// - OAError: the error information + case error(Provider, OAError) } } diff --git a/Sources/OAuthKit/OAuth.swift b/Sources/OAuthKit/OAuth.swift index 2b6bd12..4e01479 100644 --- a/Sources/OAuthKit/OAuth.swift +++ b/Sources/OAuthKit/OAuth.swift @@ -139,17 +139,7 @@ public extension OAuth { /// - pkce: the pkce data func token(provider: Provider, code: String, pkce: PKCE? = nil) { Task(priority: .high) { - let result = await requestToken(provider: provider, code: code, pkce: pkce) - switch result { - case .success(let token): - if provider.debug { - debugPrint("➡️ [Received token], [\(token)]") - } - case .failure(let error): - if provider.debug { - debugPrint("➡️ [Error requesting access token], [\(error)]") - } - } + await requestToken(provider: provider, code: code, pkce: pkce) } } @@ -280,7 +270,7 @@ private extension OAuth { schedule(provider: provider, auth: auth) case .receivedDeviceCode(let provider, let deviceCode): schedule(provider: provider, deviceCode: deviceCode) - case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode: + case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode: break } self.state = state @@ -338,20 +328,16 @@ extension OAuth { /// - provider: the provider the access token is being requested from /// - code: the code to exchange /// - pkce: the PKCE data to pass along with the request - /// - Returns: the exchange result - @discardableResult - func requestToken(provider: Provider, code: String, pkce: PKCE? = nil) async -> Result { + func requestToken(provider: Provider, code: String, pkce: PKCE? = nil) async { // Publish the state publish(state: .requestingAccessToken(provider)) guard let request = Request.token(provider: provider, code: code, pkce: pkce) else { - publish(state: .empty) - return .failure(.malformedURL) + return publish(state: .error(provider, .malformedURL)) } guard let (data, response) = try? await urlSession.data(for: request) else { - publish(state: .empty) - return .failure(.badResponse) + return publish(state: .error(provider, .badResponse)) } if provider.debug { @@ -362,19 +348,16 @@ extension OAuth { // Decode the token guard let token = try? decoder.decode(Token.self, from: data) else { - publish(state: .empty) - return .failure(.decoding) + return publish(state: .error(provider, .decoding)) } // Store the authorization let authorization = Authorization(issuer: provider.id, token: token) guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else { - publish(state: .empty) - return .failure(.keychain) + return publish(state: .error(provider, .keychain)) } publish(state: .authorized(provider, authorization)) - return .success(token) } /// Refreshes the token for the specified provider. @@ -392,7 +375,7 @@ extension OAuth { } guard let (data, response) = try? await urlSession.data(for: request) else { - return publish(state: .empty) + return publish(state: .error(provider, .badResponse)) } if provider.debug { @@ -403,13 +386,13 @@ extension OAuth { // Decode the token guard response.isOK, let token = try? decoder.decode(Token.self, from: data) else { - return publish(state: .empty) + return publish(state: .error(provider, .decoding)) } // Store the authorization let authorization = Authorization(issuer: provider.id, token: token) guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else { - return publish(state: .empty) + return publish(state: .error(provider, .keychain)) } publish(state: .authorized(provider, authorization)) } @@ -420,7 +403,7 @@ extension OAuth { func requestDeviceCode(provider: Provider) async { guard let request = Request.device(provider: provider) else { return } guard let (data, response) = try? await urlSession.data(for: request) else { - return publish(state: .empty) + return publish(state: .error(provider, .badResponse)) } if provider.debug { @@ -431,7 +414,7 @@ extension OAuth { // Decode the device code guard let deviceCode = try? decoder.decode(DeviceCode.self, from: data) else { - return publish(state: .empty) + return publish(state: .error(provider, .decoding)) } // Publish the state @@ -444,7 +427,7 @@ extension OAuth { func requestClientCredentials(provider: Provider) async { guard let request = Request.token(provider: provider) else { return } guard let (data, response) = try? await urlSession.data(for: request) else { - return publish(state: .empty) + return publish(state: .error(provider, .badResponse)) } if provider.debug { @@ -455,13 +438,13 @@ extension OAuth { // Decode the token guard let token = try? decoder.decode(Token.self, from: data) else { - return publish(state: .empty) + return publish(state: .error(provider, .decoding)) } // Store the authorization let authorization = Authorization(issuer: provider.id, token: token) guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else { - return publish(state: .empty) + return publish(state: .error(provider, .keychain)) } publish(state: .authorized(provider, authorization)) } @@ -474,11 +457,11 @@ extension OAuth { func poll(provider: Provider, deviceCode: DeviceCode) async { guard !deviceCode.isExpired, let request = Request.token(provider: provider, deviceCode: deviceCode) else { - return publish(state: .empty) + return publish(state: .error(provider, .malformedURL)) } guard let (data, response) = try? await urlSession.data(for: request) else { - return publish(state: .empty) + return publish(state: .error(provider, .badResponse)) } if provider.debug { @@ -496,7 +479,7 @@ extension OAuth { // Store the authorization let authorization = Authorization(issuer: provider.id, token: token) guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else { - return publish(state: .empty) + return publish(state: .error(provider, .keychain)) } publish(state: .authorized(provider, authorization)) } diff --git a/Sources/OAuthKit/OAuthKit.docc/Extensions/State.md b/Sources/OAuthKit/OAuthKit.docc/Extensions/State.md index 37f7b74..874515b 100644 --- a/Sources/OAuthKit/OAuthKit.docc/Extensions/State.md +++ b/Sources/OAuthKit/OAuthKit.docc/Extensions/State.md @@ -37,6 +37,8 @@ struct ContentView: View { .foregroundStyle(.blue) Text("and enter the following code:") Text(deviceCode.userCode) + case .error(let provider, let error): + Text("Error [\(provider.id)]: \(error.localizedDescription)") } } .onChange(of: oauth.state) { oldState, newState in diff --git a/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md b/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md index 6fffc4d..2ae3536 100644 --- a/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md +++ b/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md @@ -90,6 +90,8 @@ struct ContentView: View { .padding() .border(Color.primary) .font(.title) + case .error(let provider, let error): + Text("Error [\(provider.id)]: \(error.localizedDescription)") } } .onChange(of: oauth.state) { _, state in @@ -118,7 +120,7 @@ struct ContentView: View { private func handle(state: OAuth.State) { #if canImport(WebKit) switch state { - case .empty, .requestingAccessToken, .requestingDeviceCode: + case .empty, .error, .requestingAccessToken, .requestingDeviceCode: break case .authorizing, .receivedDeviceCode: openWindow(id: "oauth") @@ -140,7 +142,7 @@ OAuthKit provides an out of the box SwiftUI view ``OAWebView`` that will automat /// - Parameter state: the published state change func handle(state: OAuth.State) { switch state { - case .empty, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: + case .empty, .error, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: break case .authorizing: openWindow(id: "oauth") @@ -165,7 +167,7 @@ struct ContentView: View { var body: some View { VStack { switch oauth.state { - case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode: + case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode: EmptyView() case .authorized: Text("Authorized [\(provider.id)]") diff --git a/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-2.swift b/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-2.swift index dc4b7a4..f023b9c 100644 --- a/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-2.swift +++ b/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-2.swift @@ -44,6 +44,8 @@ struct ContentView: View { .padding() .border(Color.primary) .font(.title) + case .error(let provider, let error): + Text("Error [\(provider.id)]: \(error.localizedDescription)") } } .onChange(of: oauth.state) { _, state in diff --git a/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-3.swift b/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-3.swift index ba0178a..751570c 100644 --- a/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-3.swift +++ b/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-3.swift @@ -58,6 +58,8 @@ struct ContentView: View { .padding() .border(Color.primary) .font(.title) + case .error(let provider, let error): + Text("Error [\(provider.id)]: \(error.localizedDescription)") } } .onChange(of: oauth.state) { _, state in diff --git a/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-4.swift b/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-4.swift index eb75dc3..c2e2239 100644 --- a/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-4.swift +++ b/Sources/OAuthKit/OAuthKit.docc/Resources/tutorial-code-files/flows-states-step-4.swift @@ -24,7 +24,7 @@ struct ContentView: View { private func handle(state: OAuth.State) { #if canImport(WebKit) switch state { - case .empty, .requestingAccessToken, .requestingDeviceCode: + case .empty, .error, .requestingAccessToken, .requestingDeviceCode: break case .authorizing, .receivedDeviceCode: openWindow(id: "oauth") diff --git a/Sources/OAuthKit/Views/OAWebViewCoordinator.swift b/Sources/OAuthKit/Views/OAWebViewCoordinator.swift index 8f23c82..ad7c08e 100644 --- a/Sources/OAuthKit/Views/OAWebViewCoordinator.swift +++ b/Sources/OAuthKit/Views/OAWebViewCoordinator.swift @@ -75,7 +75,7 @@ public class OAWebViewCoordinator: NSObject { /// - Parameter state: the published state change. func update(state: OAuth.State) { switch state { - case .empty, .authorized, .requestingAccessToken, .requestingDeviceCode: + case .empty, .error, .authorized, .requestingAccessToken, .requestingDeviceCode: break case .authorizing(let provider, let grantType): // Override the custom user agent for the provider and tell the browser to load the request @@ -99,7 +99,7 @@ extension OAWebViewCoordinator: WKNavigationDelegate { public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { guard let url = navigationAction.request.url else { return .cancel } switch oauth.state { - case .empty, .requestingAccessToken, .authorized, .requestingDeviceCode, .receivedDeviceCode: + case .empty, .error, .requestingAccessToken, .authorized, .requestingDeviceCode, .receivedDeviceCode: break case .authorizing(let provider, let grantType): handle(url: url, provider: provider, grantType: grantType) diff --git a/Tests/OAuthKitTests/OAWebViewTests.swift b/Tests/OAuthKitTests/OAWebViewTests.swift index dcfaa52..e8ff289 100644 --- a/Tests/OAuthKitTests/OAWebViewTests.swift +++ b/Tests/OAuthKitTests/OAWebViewTests.swift @@ -143,7 +143,7 @@ final class OAWebViewTests { let monitor: OAuth.Monitor = .init(oauth: oauth) for await state in monitor.stream { switch state { - case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: + case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: break case .authorized(_, _): keychain.clear() diff --git a/Tests/OAuthKitTests/OAuth+Monitor.swift b/Tests/OAuthKitTests/OAuth+Monitor.swift index 50c680e..02893e3 100644 --- a/Tests/OAuthKitTests/OAuth+Monitor.swift +++ b/Tests/OAuthKitTests/OAuth+Monitor.swift @@ -45,7 +45,7 @@ extension OAuth { let state = oauth.state continuation?.yield(state) switch state { - case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: + case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: waitForNextValue() case .authorized(_, _): continuation?.finish() diff --git a/Tests/OAuthKitTests/OAuthTests.swift b/Tests/OAuthKitTests/OAuthTests.swift index 28a5cd7..0ffe63f 100644 --- a/Tests/OAuthKitTests/OAuthTests.swift +++ b/Tests/OAuthKitTests/OAuthTests.swift @@ -260,29 +260,29 @@ final class OAuthTests { // Token let oauth: OAuth = .init(.module, options: options) - let result = await oauth.requestToken(provider: provider, code: .secureRandom()) - #expect(result == .failure(.decoding)) + await oauth.requestToken(provider: provider, code: .secureRandom()) + #expect(oauth.state == .error(provider, .decoding)) // Refresh Token let token: OAuth.Token = .init(accessToken: .secureRandom(), refreshToken: .secureRandom(), expiresIn: 0, scope: "email", type: "Bearer") let auth: OAuth.Authorization = .init(issuer: provider.id, token: token) try! oauth.keychain.set(auth, for: provider.id) await oauth.refreshToken(provider: provider) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .decoding)) oauth.clear() // Client Credentials await oauth.requestClientCredentials(provider: provider) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .decoding)) // Device Code await oauth.requestDeviceCode(provider: provider) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .decoding)) // Device Code Polling let deviceCode: OAuth.DeviceCode = .init(deviceCode: .secureRandom(), userCode: .secureRandom(), verificationUri: "https://example.com", expiresIn: 200, interval: 0) await oauth.poll(provider: provider, deviceCode: deviceCode) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .decoding)) } @Test("When Received server error") @@ -301,29 +301,29 @@ final class OAuthTests { // Token let oauth: OAuth = .init(.module, options: options) - let result = await oauth.requestToken(provider: provider, code: .secureRandom()) - #expect(result == .failure(.badResponse)) + await oauth.requestToken(provider: provider, code: .secureRandom()) + #expect(oauth.state == .error(provider, .badResponse)) // Refresh Token let token: OAuth.Token = .init(accessToken: .secureRandom(), refreshToken: .secureRandom(), expiresIn: 0, scope: "email", type: "Bearer") let auth: OAuth.Authorization = .init(issuer: provider.id, token: token) try! oauth.keychain.set(auth, for: provider.id) await oauth.refreshToken(provider: provider) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .badResponse)) oauth.clear() // Client Credentials await oauth.requestClientCredentials(provider: provider) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .badResponse)) // Device Code await oauth.requestDeviceCode(provider: provider) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .badResponse)) // Device Code Polling let deviceCode: OAuth.DeviceCode = .init(deviceCode: .secureRandom(), userCode: .secureRandom(), verificationUri: "https://example.com", expiresIn: 200, interval: 0) await oauth.poll(provider: provider, deviceCode: deviceCode) - #expect(oauth.state == .empty) + #expect(oauth.state == .error(provider, .badResponse)) } /// Tests the PKCE request parameters. @@ -427,7 +427,7 @@ final class OAuthTests { let monitor: OAuth.Monitor = .init(oauth: oauth) for await state in monitor.stream { switch state { - case .empty, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: + case .empty, .error, .authorizing, .requestingAccessToken, .requestingDeviceCode, .receivedDeviceCode: break case .authorized(_, _): oauth.clear()