Description
Similar to this post we are trying to use OAuth and the Zoom Rest API in our mobile iOS app. We are having issues getting the OAuth to work in iOS. Any help is greatly appreciated!
Issue 1: Universal Link Redirects
Our plan was to use AppAuth-iOS [1] with Universal Links [2].
NOTE: AppAuth-iOS uses a ASWebAuthenticationSession
to present the browser for logging in.
We are having issues with the Universal Link redirection, the Universal Link catches the redirect
when using “Sign-in with Google” but not when using “Sign-in”. See image below.
We would prefer to use Universal Links instead of using a custom URI scheme (iOS deeplinks) for two reasons:
- This is preferred method laid out by RFC 8252 Appendix B.1 [3]
- Zoom OAuth does not support custom URI schemes, only
https
. To use a custom URI scheme we would have to have a server redirect fromhttps://<redirect_uri>
to<custom scheme>://<redirect_uri>
as suggested by this comment. This unfortunately does not play well with the AppAuth-iOS library since they have checks to ensure that the redirects match.
We can work around the above to 2 and use the server to redirect (just more work on our end). But I am curious if there is a way to get Universal Links to work with Zoom. It appears that “Sign-in with Google” uses a HTTP 302 response to trigger the redirect to the redirect_uri
where as “Sign-in with Zoom” appears to use a client side redirect which does not seem to trigger the Universal Link (except the first time the user authorizes and they get the authorization prompt, in that case the Universal Link works).
Any ideas on how we could get Universal Links to work would be greatly appreciated as this is our preferred method.
Issue 2: Client Secret and Code Exchange
It appears that Zoom does not support PKCE [4] which would be recommended way for a native app to perform the OAuth exchange as it does not involve a client secret. Given this limitation it appears that the only secure way for our App to complete the OAuth exchange would be for us to create a server to perform the code exchange as described in this comment [6] (please see extra links since Zoom prevents more then 2 links in a post for new users).
Is there a simpler way of achieving the same result? It appears OAuth also has an implicit flow [5, section-1.3.2], however it sounds like that may not be secure enough or appropriate for our use case.
We have also noticed that there is 2 potential authorization urls, is there any deference between these 2?
https://zoom.us/oauth2/login
https://zoom.us/oauth/authorize
Extra Links
(due to the 2 link new user limit)
https://github.com/openid/AppAuth-iOS
https://developer.apple.com/documentation/xcode/allowing_apps_and_websites_to_link_to_your_content?language=objc
https://tools.ietf.org/html/rfc8252#appendix-B.1
https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce
https://tools.ietf.org/html/rfc6749#section-1.3.2
https://devforum.zoom.us/t/oauth-with-zoom-rest-api-from-ios/23937/4
Sample Code
Here is a stripped down example of what we doing.
NOTE: for simplicity this example code has the FE performing the code exchange.
import Foundation
import SwiftUI
import AuthenticationServices
import AppAuth
extension SceneDelegate {
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
print(userActivity)
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let authorizationFlow = appDelegate.currentAuthorizationFlow,
authorizationFlow.resumeExternalUserAgentFlow(with: url) {
appDelegate.currentAuthorizationFlow = nil
}
}
}
}
extension UIApplication {
func getTopViewController() -> UIViewController? {
let keyWindow = self.windows.first(where: { $0.isKeyWindow })
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
return keyWindow?.rootViewController
}
}
func presentAuthSession() {
let configuration = OIDServiceConfiguration(
authorizationEndpoint: URL(string: "https://zoom.us/oauth2/login")!,
tokenEndpoint: URL(string: "https://zoom.us/oauth/token")!
)
// builds authentication request
let request = OIDAuthorizationRequest(
configuration: configuration,
clientId: "*************",
clientSecret: "**************",
scope: nil,
redirectURL: URL(string: "https://oauthexample.timeless.space/zoom")!,
responseType: "code",
state: nil,
nonce: nil,
codeVerifier: nil,
codeChallenge: nil,
codeChallengeMethod: nil,
additionalParameters: nil
)
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let presentingController = UIApplication.shared.getTopViewController() {
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
byPresenting: request,
presenting: presentingController
) { authState, error in
guard error == nil else {
print("Zoom Authorization Error \(error!)")
return
}
if authState != nil {
print("Success!!")
}
}
} else {
print("Cannot find top view controller")
}
}
struct ContentView: View {
var body: some View {
Button("Login", action: presentAuthSession)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}