-
Notifications
You must be signed in to change notification settings - Fork 228
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
feat: added support for webview as a provider for webauth #875
Changes from 4 commits
21a691d
017e31b
843703b
351ffaf
a519581
902e181
add7f43
405d22b
e2f23cb
e7fc8ff
77840f7
73160d2
7e6135d
bddf7a5
b8de698
eeb0e40
aa8441f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
#if WEB_AUTH_PLATFORM | ||
import Foundation | ||
import Combine | ||
#if os(iOS) | ||
import UIKit | ||
#endif | ||
|
||
final class Auth0WebAuth: WebAuth { | ||
|
||
|
@@ -33,6 +36,11 @@ | |
private(set) var overrideAuthorizeURL: URL? | ||
private(set) var provider: WebAuthProvider? | ||
private(set) var onCloseCallback: (() -> Void)? | ||
|
||
#if os(iOS) | ||
private(set) var useWebViewProvider = false | ||
private(set) var webViewProviderPresentationStyle: UIModalPresentationStyle = .fullScreen | ||
#endif | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a func like https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift#L37 also has the advantage of being on its own (gated) file, so there's no need to have these conditionals here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed accordingly |
||
|
||
var state: String { | ||
return self.parameters["state"] ?? self.generateDefaultState() | ||
|
@@ -144,6 +152,14 @@ | |
self.ephemeralSession = true | ||
return self | ||
} | ||
|
||
#if os(iOS) | ||
func useWebViewProvider(style: UIModalPresentationStyle = .fullScreen) -> Self { | ||
self.useWebViewProvider = true | ||
self.webViewProviderPresentationStyle = style | ||
return self | ||
} | ||
#endif | ||
|
||
func invitationURL(_ invitationURL: URL) -> Self { | ||
self.invitationURL = invitationURL | ||
|
@@ -196,8 +212,12 @@ | |
state: state, | ||
organization: organization, | ||
invitation: invitation) | ||
let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, | ||
ephemeralSession: ephemeralSession) | ||
|
||
#if os(iOS) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed accordingly |
||
let provider = useWebViewProvider ? WebAuthentication.webViewProvider(redirectionURL: redirectURL, style: webViewProviderPresentationStyle) : (self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, ephemeralSession: ephemeralSession)) | ||
#else | ||
let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, ephemeralSession: ephemeralSession) | ||
#endif | ||
let userAgent = provider(authorizeURL) { [storage, onCloseCallback] result in | ||
storage.clear() | ||
|
||
|
@@ -238,7 +258,11 @@ | |
return callback(.failure(WebAuthError(code: .noBundleIdentifier))) | ||
} | ||
|
||
#if os(iOS) | ||
let provider = useWebViewProvider ? WebAuthentication.webViewProvider(redirectionURL: redirectURL, style: webViewProviderPresentationStyle) : (self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL)) | ||
#else | ||
let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL) | ||
#endif | ||
let userAgent = provider(logoutURL) { [storage] result in | ||
storage.clear() | ||
callback(result) | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,6 +5,10 @@ | |||||||||||||||||
public struct WebAuthError: Auth0Error { | ||||||||||||||||||
|
||||||||||||||||||
enum Code: Equatable { | ||||||||||||||||||
case webViewNavigationFailed | ||||||||||||||||||
case webViewProvisionalNavigationFailed | ||||||||||||||||||
case webViewContentProcessTerminated | ||||||||||||||||||
case webViewResourceLoadingStopped | ||||||||||||||||||
case noBundleIdentifier | ||||||||||||||||||
case transactionActiveAlready | ||||||||||||||||||
case invalidInvitationURL(String) | ||||||||||||||||||
|
@@ -39,7 +43,7 @@ | |||||||||||||||||
/// build a valid URL. | ||||||||||||||||||
/// This error does not include a ``Auth0Error/cause-9wuyi``. | ||||||||||||||||||
public static let noBundleIdentifier: WebAuthError = .init(code: .noBundleIdentifier) | ||||||||||||||||||
|
||||||||||||||||||
/// There is already an active transaction at the moment; therefore, this newly initiated transaction is canceled. | ||||||||||||||||||
/// This error does not include a ``Auth0Error/cause-9wuyi``. | ||||||||||||||||||
public static let transactionActiveAlready: WebAuthError = .init(code: .transactionActiveAlready) | ||||||||||||||||||
|
@@ -82,6 +86,10 @@ | |||||||||||||||||
|
||||||||||||||||||
var message: String { | ||||||||||||||||||
switch self.code { | ||||||||||||||||||
case .webViewNavigationFailed: return "An error occured during a committed main frame navigation of WebView" | ||||||||||||||||||
case .webViewProvisionalNavigationFailed: return "An error occured while starting to load data for the main frame of WebView" | ||||||||||||||||||
case .webViewContentProcessTerminated: return "WebView's content process is terminated." | ||||||||||||||||||
case .webViewResourceLoadingStopped: return "WebView's resource loading has been stopped" | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated accordingly |
||||||||||||||||||
case .noBundleIdentifier: return "Unable to retrieve the bundle identifier from Bundle.main.bundleIdentifier," | ||||||||||||||||||
+ " or it could not be used to build a valid URL." | ||||||||||||||||||
case .transactionActiveAlready: return "Failed to start this transaction, as there is an active transaction at the" | ||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// | ||
// WebViewProvider.swift | ||
// Auth0 | ||
// | ||
// Created by Desu Sai Venkat on 18/09/24. | ||
// Copyright © 2024 Auth0. All rights reserved. | ||
// | ||
|
||
#if os(iOS) | ||
|
||
@preconcurrency import WebKit | ||
|
||
|
||
extension WebAuthentication { | ||
static func webViewProvider(redirectionURL: URL, style: UIModalPresentationStyle = .fullScreen) -> WebAuthProvider { | ||
return { url, callback in | ||
WebViewUserAgent(authorizeURL: url, redirectURL: redirectionURL, modalPresentationStyle: style, callback: callback) | ||
} | ||
} | ||
} | ||
|
||
extension WebViewUserAgent { | ||
var topViewController: UIViewController? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have this logic here: https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift#L50-L77 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed, moved this to a shared helper |
||
guard let root = UIApplication.shared()?.windows.last(where: \.isKeyWindow)?.rootViewController else { | ||
return nil | ||
} | ||
return self.findTopViewController(from: root) | ||
} | ||
|
||
private func findTopViewController(from root: UIViewController) -> UIViewController? { | ||
if let presented = root.presentedViewController { return self.findTopViewController(from: presented) } | ||
|
||
switch root { | ||
case let split as UISplitViewController: | ||
guard let last = split.viewControllers.last else { return split } | ||
return self.findTopViewController(from: last) | ||
case let navigation as UINavigationController: | ||
guard let top = navigation.topViewController else { return navigation } | ||
return self.findTopViewController(from: top) | ||
case let tab as UITabBarController: | ||
guard let selected = tab.selectedViewController else { return tab } | ||
return self.findTopViewController(from: selected) | ||
default: | ||
return root | ||
} | ||
} | ||
} | ||
|
||
class WebViewUserAgent: NSObject, WebAuthUserAgent { | ||
|
||
static let customSchemeRedirectionSuccessMessage = "com.auth0.webview.redirection_success" | ||
static let customSchemeRedirectionFailureMessage = "com.auth0.webview.redirection_failure" | ||
let defaultSchemesSupportedByWKWebview = ["http", "https"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest checking with somebody from the security team if we should be supporting |
||
|
||
let request: URLRequest | ||
var webview: WKWebView! | ||
let viewController: UIViewController | ||
let redirectURL: URL | ||
let callback: WebAuthProviderCallback | ||
|
||
|
||
init(authorizeURL: URL, redirectURL: URL, viewController: UIViewController = UIViewController(), modalPresentationStyle: UIModalPresentationStyle = .fullScreen, callback: @escaping WebAuthProviderCallback) { | ||
self.request = URLRequest(url: authorizeURL) | ||
self.redirectURL = redirectURL | ||
self.callback = callback | ||
self.viewController = viewController | ||
self.viewController.modalPresentationStyle = modalPresentationStyle | ||
|
||
super.init() | ||
if !defaultSchemesSupportedByWKWebview.contains(redirectURL.scheme!) { | ||
self.setupWebViewWithCustomScheme() | ||
} else { | ||
self.setupWebViewWithHTTPS() | ||
} | ||
} | ||
|
||
private func setupWebViewWithCustomScheme() { | ||
let configuration = WKWebViewConfiguration() | ||
configuration.setURLSchemeHandler(self, forURLScheme: redirectURL.scheme!) | ||
self.webview = WKWebView(frame: .zero, configuration: configuration) | ||
self.viewController.view = webview | ||
webview.navigationDelegate = self | ||
} | ||
|
||
private func setupWebViewWithHTTPS() { | ||
self.webview = WKWebView(frame: .zero) | ||
self.viewController.view = webview | ||
webview.navigationDelegate = self | ||
} | ||
|
||
func start() { | ||
self.webview.load(self.request) | ||
self.topViewController?.present(self.viewController, animated: true) | ||
} | ||
|
||
func finish(with result: WebAuthResult<Void>) { | ||
DispatchQueue.main.async { [weak webview, weak viewController, callback] in | ||
webview?.removeFromSuperview() | ||
guard let presenting = viewController?.presentingViewController else { | ||
let error = WebAuthError(code: .unknown("Cannot dismiss WKWebView")) | ||
return callback(.failure(error)) | ||
} | ||
presenting.dismiss(animated: true) { | ||
callback(result) | ||
} | ||
} | ||
} | ||
|
||
public override var description: String { | ||
return String(describing: WKWebView.self) | ||
} | ||
} | ||
|
||
/// Handling Custom Scheme Callbacks | ||
extension WebViewUserAgent: WKURLSchemeHandler { | ||
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { | ||
_ = TransactionStore.shared.resume(urlSchemeTask.request.url!) | ||
let error = NSError(domain: WebViewUserAgent.customSchemeRedirectionSuccessMessage, code: 200, userInfo: [ | ||
NSLocalizedDescriptionKey: "WebViewProvider: WKURLSchemeHandler: Succesfully redirected back to the app" | ||
]) | ||
urlSchemeTask.didFailWithError(error) | ||
} | ||
|
||
func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { | ||
let error = NSError(domain: WebViewUserAgent.customSchemeRedirectionFailureMessage, code: 400, userInfo: [ | ||
NSLocalizedDescriptionKey: "WebViewProvider: WKURLSchemeHandler: Webview Resource Loading has been stopped" | ||
]) | ||
urlSchemeTask.didFailWithError(error) | ||
self.finish(with: .failure(WebAuthError(code: .webViewResourceLoadingStopped))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this error case be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, updated accordingly |
||
} | ||
} | ||
|
||
/// Handling HTTPS Callbacks | ||
extension WebViewUserAgent: WKNavigationDelegate { | ||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { | ||
if let callbackUrl = navigationAction.request.url, callbackUrl.absoluteString.starts(with: redirectURL.absoluteString) { | ||
_ = TransactionStore.shared.resume(callbackUrl) | ||
decisionHandler(.cancel) | ||
} else { | ||
decisionHandler(.allow) | ||
} | ||
} | ||
|
||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) { | ||
if (error as NSError).domain == WebViewUserAgent.customSchemeRedirectionSuccessMessage { | ||
return | ||
} | ||
self.finish(with: .failure(WebAuthError(code: .webViewNavigationFailed, cause: error))) | ||
} | ||
|
||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) { | ||
if (error as NSError).domain == WebViewUserAgent.customSchemeRedirectionSuccessMessage { | ||
return | ||
} | ||
self.finish(with: .failure(WebAuthError(code: .webViewProvisionalNavigationFailed, cause: error))) | ||
} | ||
|
||
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { | ||
self.finish(with: .failure(WebAuthError(code: .webViewContentProcessTerminated))) | ||
} | ||
} | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest adding a new static func to the
WebAuthentication
struct like https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift#L37, as that:useX()
methods that have the disadvantage of being mutually exclusive (e.g. these can be chained and it's not clear which one will end up being used)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, changed accordingly