diff --git a/README.md b/README.md index bcf1636..8c2c356 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,19 @@ import SwiftUI @main struct OAuthApp: App { + /// The observable oauth object + let oauth: OAuth = .init(.main) + /// Build the scene body var body: some Scene { WindowGroup { ContentView() } + .environment(\.oauth, oauth) WindowGroup(id: "oauth") { - OAWebView() + OAWebView(oauth: oauth) } } } diff --git a/Sources/OAuthKit/Views/OAWebView.swift b/Sources/OAuthKit/Views/OAWebView.swift index a3989fb..8ad5c30 100644 --- a/Sources/OAuthKit/Views/OAWebView.swift +++ b/Sources/OAuthKit/Views/OAWebView.swift @@ -14,12 +14,14 @@ import WebKit @MainActor public struct OAWebView { - @Environment(\.oauth) - var oauth: OAuth + let oauth: OAuth let view = WKWebView() - /// Public Initializer. - public init() { } + /// Initializer with the speciifed oauth object, + /// - Parameter oauth: the oauth object to use + public init(oauth: OAuth) { + self.oauth = oauth + } public func makeWebView(context: Context) -> WKWebView { view.navigationDelegate = context.coordinator diff --git a/Sources/OAuthKit/Views/OAWebViewCoordinator.swift b/Sources/OAuthKit/Views/OAWebViewCoordinator.swift index cd82810..d9ec6a8 100644 --- a/Sources/OAuthKit/Views/OAWebViewCoordinator.swift +++ b/Sources/OAuthKit/Views/OAWebViewCoordinator.swift @@ -55,10 +55,6 @@ public class OAWebViewCoordinator: NSObject { } // Exchange the code for a token oauth.token(provider: provider, code: code) - case .clientCredentials: - break - case .deviceCode: - break case .pkce(let pkce): // Verify the state guard state == pkce.state else { @@ -70,7 +66,7 @@ public class OAWebViewCoordinator: NSObject { } // Exchange the code for a token along with the pkce validation data oauth.token(provider: provider, code: code, pkce: pkce) - case .refreshToken: + case .clientCredentials, .deviceCode, .refreshToken: break } } diff --git a/Tests/OAuthKitTests/OAWebViewTests.swift b/Tests/OAuthKitTests/OAWebViewTests.swift index 9de3f98..d5b24d2 100644 --- a/Tests/OAuthKitTests/OAWebViewTests.swift +++ b/Tests/OAuthKitTests/OAWebViewTests.swift @@ -25,10 +25,12 @@ final class OAWebViewTests { }() let oauth: OAuth + let webView: OAWebView /// Initializer. init() async throws { oauth = .init(.module) + webView = .init(oauth: oauth) oauth.urlSession = urlSession } @@ -42,23 +44,83 @@ final class OAWebViewTests { #expect(environmentOAuth == oauth) } - /// Tests the OAWebViewCoordinator coordinator policy decision. - /// TODO: This is fairly limited at the moment because we will receive errors - /// about accessing the `oauth` environment outside of being installed on a view. - /// Needs more investigation for testing SwiftUI views. + /// Tests the OAWebViewCoordinator coordinator policy decisions. @Test("Coordinator Policy Decisons") func whenCoordinatorDecidingPolicy() async throws { - let webView: OAWebView = .init() + // 1) Bad Request Expectations let coordinator: OAWebViewCoordinator = webView.makeCoordinator() + #expect(coordinator.oauth == oauth) let wkWebView = webView.view var urlRequest: URLRequest = .init(url: URL(string: "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/codefiesta/OAuthKit")!) urlRequest.url = nil - let navigationAction: WKNavigationAction = OAuthTestWKNavigationAction(urlRequest: urlRequest) - let policy = await coordinator.webView(wkWebView, decidePolicyFor: navigationAction) + var navigationAction: WKNavigationAction = OAuthTestWKNavigationAction(urlRequest: urlRequest) + var policy = await coordinator.webView(wkWebView, decidePolicyFor: navigationAction) #expect(policy == .cancel) + + let provider = oauth.providers[0] + + // 2) Authorization Code Expectations + let state: String = .secureRandom() + let code: String = .secureRandom() + + oauth.authorize(provider: provider, grantType: .authorizationCode(state)) + coordinator.update(state: oauth.state) + var urlString = provider.redirectURI! + "?code=\(code)&state=\(state)" + urlRequest = .init(url: URL(string: urlString)!) + + navigationAction = OAuthTestWKNavigationAction(urlRequest: urlRequest) + policy = await coordinator.webView(wkWebView, decidePolicyFor: navigationAction) + #expect(policy == .allow) + + // 3) PKCE Expectations + let pkce: OAuth.PKCE = .init() + oauth.authorize(provider: provider, grantType: .pkce(pkce)) + coordinator.update(state: oauth.state) + urlString = provider.redirectURI! + "?code=\(code)&state=\(pkce.state)" + urlRequest = .init(url: URL(string: urlString)!) + + navigationAction = OAuthTestWKNavigationAction(urlRequest: urlRequest) + policy = await coordinator.webView(wkWebView, decidePolicyFor: navigationAction) + #expect(policy == .allow) + } + + /// Tests to make sure the coordinator doesn't being requesting access tokens when we've detected state mismatches. + @Test("Coordinator Detects Mismatched States") + func whenCoordinatorDetectsMismatchedStates() async throws { + + // 1) Bad Request Expectations + let coordinator: OAWebViewCoordinator = webView.makeCoordinator() + #expect(coordinator.oauth == oauth) + let wkWebView = webView.view + + var urlRequest: URLRequest = .init(url: URL(string: "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/codefiesta/OAuthKit")!) + let provider = oauth.providers[0] + + // 2) Authorization Code Expectations + let state: String = .secureRandom() + let code: String = .secureRandom() + + oauth.authorize(provider: provider, grantType: .authorizationCode(state)) + let urlString = provider.redirectURI! + "?code=\(code)&state=ABC-123" + urlRequest = .init(url: URL(string: urlString)!) + var navigationAction: WKNavigationAction = OAuthTestWKNavigationAction(urlRequest: urlRequest) + var policy = await coordinator.webView(wkWebView, decidePolicyFor: navigationAction) + #expect(policy == .allow) + #expect(oauth.state != .requestingAccessToken(provider)) + + // 3) PKCE Expectations + let pkce: OAuth.PKCE = .init() + oauth.authorize(provider: provider, grantType: .pkce(pkce)) + coordinator.update(state: oauth.state) + + navigationAction = OAuthTestWKNavigationAction(urlRequest: urlRequest) + policy = await coordinator.webView(wkWebView, decidePolicyFor: navigationAction) + #expect(policy == .allow) + #expect(oauth.state != .requestingAccessToken(provider)) + } } #endif