Session completion handler not being called for ASWebAuthenticationSession - ios

I am trying to use ASWebAuthenticationSession web view. After the authentication is complete, the session completion handler is not being called. Hence, the web view doesn't dismiss.
guard let authURL = URL(string: "https://github.com/login/oauth/authorize?client_id=<client_id>/")
else { return }
let scheme = "octonotes"
session = ASWebAuthenticationSession.init(url: authURL, callbackURLScheme: scheme, completionHandler: { callbackURL, error in
// Handle the callback.
print(callbackURL!)
print(error!)
})
session?.presentationContextProvider = self
session?.start()
I have set the callback url scheme in info.plist. The same is updated in Targets -> info -> URL Types
It looks like:
URL Types
After running the above code, ASWebAuthenticationSession web view is presented, which provides user with sign in page. Once the authentication is complete, web view does not dismiss unlike WKWebView.
There is cancel option on top left of the web view, it calls the completion handler with error.
Is there a way to dismiss webview after the authentication session is complete?

It looks like you're missing the redirect URI in your authURL - so GitHub doesn't know where to redirect the successful auth to.
Try this:
let scheme = "octonotes"
guard let authURL = URL(string: "https://github.com/login/oauth/authorize?client_id=<client_id>&redirect_uri=\(scheme)://authcallback")
Bear in mind you may be missing some other parameters too, take a look here for the other ones you can provide https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls

Related

AppAuth caching issue / universal link not caught when logging out quickly

On iOS 13.5 with the latest AppAuth (1.4.0), I have a weird caching / universal link issue with logging in through AppAuth, logging out and logging back in again. Based on the documentation, I first discover the configuration from the server with AppAuth:
OIDAuthorizationService.discoverConfiguration(forIssuer: URL(string: "https://identityserver.example.com/")!) { ... }
Then, I build a new request:
let signinRedirectURL = URL(string: "https://portal.example.com/signin-oidc-ios")!
let request = OIDAuthorizationRequest(configuration: config,
clientId: "ios-app",
scopes: ["api"],
redirectURL: signinRedirectURL,
responseType: OIDResponseTypeCode,
additionalParameters: nil)
and present it:
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in self.processAuthState(authState, error) }
After logging in through the in-app browser popup, the universal link is processed:
if let authorizationFlow = appDelegate.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) {
appDelegate.currentAuthorizationFlow = nil
} else {
print("...")
}
Finally I process the received authState:
func processAuthState(authState: OIDAuthState, error: Error) {
if let authState = authState, let token = authState.lastTokenResponse?.accessToken {
appDelegate.authState = authState
self.accessToken = token // stored later on for usage by REST API
} else {
print("Authorization error: \(error?.localizedDescription ?? "Unknown error")")
}
}
When logging out, I simply throw away the authState and currentAuthorizationFlow. Then, to log in again, the same process begins again.
The weird thing now is that AppAuth does not present a login in-app-browser popup with the login mask at https://identityserver.example.com/ as before in the first login attempt after each app launch, but instead it presents that same popup with the universal link like https://portal.example.com/signin-oidc-ios?code=abcdef&scope=api&state=xyz which was previously caught by iOS and forwarded to the app leading to the call to authorizationFlow.resumeExternalUserAgentFlow(with: url) from above.
Because we have not implemented the universal link fully yet, it leads to an error message, because the URL with the link is not supposed to be called in the browser in the moment but only to communicate the token to the app through the universal link mechanism.
Why does AppAuth or ASWebAuthenticationSession seemingly cache the last URL with an old token from the previous login attempt within the same app launch even though I throw away both the authState and currentAuthorizationFlow and create new ones? Is there something else I should do to "log out", clear the cookies etc?

taskWillPerformHTTPRedirection never called in Alamofire 5

Updating Alamofire to 5.0.4. As the title says taskWillPerformHTTPRedirection is never called.
In Alamofire 4.x we could do something like:
let sessionDelegate = request.session.delegate as! Alamofire.SessionDelegate
sessionDelegate.taskWillPerformHTTPRedirection = { session, task, response, request in
if let url = task.currentRequest?.url {
// look at redirected url & act accordingly
}
}
}
A request's session/delegate has been overhauled in Alamofire 5 and is no longer directly accessible from the request. More specifically, taskWillPerformHTTPRedirection is a closure callback on ClosureEventMonitor. As a sanity check, I tested using some of the other closure callbacks.. and they worked.
// ClosureEventMonitor
let monitor = ClosureEventMonitor()
monitor.requestDidCreateTask = { request, task in
// Event fires
}
let monitor2 = ClosureEventMonitor()
monitor2.taskWillPerformHTTPRedirection = { sess, task, resp, req in
// Event Never fires
}
monitor2.requestDidFinish = { request in
// Event Fires
}
// Set up Session
var session: Session? = Session(startRequestsImmediately: false, eventMonitors: [monitor, monitor2])
let url = URL(string: "https://google.com")!
let urlRequest = URLRequest(url: url)
let trequest = session?.request(urlRequest)
For reference this code is being fired from my AppDelegate func application(_ application: UIApplication, continue userActivity: NSUserActivity for handling deep/universal links.
I'm not exactly sure what I'm missing here. Any help is greatly appreciated. Thank you for your time.
There are three things here:
First, session?.request(urlRequest) will never actually make a request, since you never call resume() (or attach a response handler).
Second, using a one off Session like that is not recommended. As soon as the Session goes out of scope all requests will be cancelled.
Third, EventMonitors cannot interact with the request pipeline, they're only observational. Instead, use Alamofire 5's new RedirectHandler protocol or Redirector type to handle redirects. There is more in our documentation. A simple implementation that customizes the action performed would be:
let redirector = Redirector(behavior: .modify { task, request, response in
// Customize behavior.
})
session?.request(urlRequest).redirect(using: redirector)

Spotify SessionManager continually failing with error "invalid_grant"

What I'm trying to do:
I am implementing the Spotify SDK into my iOS project. I am successfully receiving access tokens for Spotify's API as I am able to do things like search artists, search songs, and view playlists using said API.
The one thing I am struggling to do is play music with the SDK. I have a button that, upon clicking, I want the following flow to happen:
I request Spotify access by doing the following function and using the following Session Manager:
let SpotifyClientID = "###"
let SpotifyRedirectURL = URL(string: "bandmate://")!
lazy var configuration = SPTConfiguration(
clientID: SpotifyClientID,
redirectURL: SpotifyRedirectURL
)
lazy var sessionManager: SPTSessionManager = {
if let tokenSwapURL = URL(string: "https://bandmateallcaps.herokuapp.com/api/token"),
let tokenRefreshURL = URL(string: "https://bandmateallcaps.herokuapp.com/api/refresh_token") {
configuration.tokenSwapURL = tokenSwapURL
configuration.tokenRefreshURL = tokenRefreshURL
configuration.playURI = ""
}
let manager = SPTSessionManager(configuration: configuration, delegate: self)
return manager
}()
func requestSpotifyAccess() {
let requestedScopes: SPTScope = [.appRemoteControl, .userReadPrivate]
self.sessionManager.initiateSession(with: requestedScopes, options: .default)
}
Upon initiation of a SPTSession, I want to connect my remote:
lazy var appRemote: SPTAppRemote = {
let appRemote = SPTAppRemote(configuration: configuration, logLevel: .debug)
appRemote.delegate = self
return appRemote
}()
func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
self.appRemote.connectionParameters.accessToken = session.accessToken
self.appRemote.connect()
}
Upon app connection, I want to play the ID of a Spotify track that is declared globally:
var pendingSpotifyId: String!
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
print("connected")
self.appRemote.playerAPI!.delegate = self
self.appRemote.playerAPI!.subscribe(toPlayerState: { (result, error) in
if let error = error {
debugPrint(error.localizedDescription)
} else if self.pendingSpotifyId != nil {
self.appRemote.playerAPI!.play(self.pendingSpotifyId, callback: { (any, err) in
self.pendingSpotifyId = nil
})
}
})
}
My problem:
This flow is broken up as any time I try to initiate a session, sessionManager(manager: SPTSessionManager, didFailWith error: Error) is always called returning the following error:
Error Domain=com.spotify.sdk.login Code=1 "invalid_grant" UserInfo={NSLocalizedDescription=invalid_grant}
I need the session to initiate successfully so that sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) can be called and I can connect my remote and, ultimately, play my Spotify track.
What I've tried:
I have ensured a number of things:
Ensured the state of the Spotify app in the background on the user's device is playing (per this ticket: https://github.com/spotify/ios-sdk/issues/31)
Ensured that the correct scopes are in place when receiving an access token. Returned JSON looks something like:
{"access_token":"###","token_type":"Bearer","expires_in":3600,"refresh_token":"###","scope":"app-remote-control user-read-private"}
Things I'm suspicious of:
I am unaware if my token swap via Heroku is being done correctly. This is the only reason I can think of as to why I would be getting this issue. If I am able to use the Spotify API, is this evidence enough that my token swap is being done correctly? (I suspect it is)
Here's what we found out, hope it will help:
The SpotifySDK tutorial doesn't mention that Bundle ID and App Callback URL must precisely match across App Info.plist, source code, Spotify App Dashboard and Heroku Env Vars. The Bundle ID used must match your application Bundle ID.
The App Callback URL must not have empty path, ie: my-callback-scheme://spotify-login-callback
When using both Web Spotify SDK and iOS Framework Spotify SDK in app, take care that only one of them performs auth. Otherwise the App Callback URL will be called twice resulting in error.
The Spotify configuration.playURI may need to be set to empty string rather than nil. Sample app has a note on it.
It's best to have only one instance of object managing Spotify auth in the app. Otherwise ensuring that the correct object is called from the AppDelegate open url method can be tricky.

How to auto close a Safari View Controller or Chrome Custom Tab and return to the mobile app?

My mobile app (React Native) uses a Web View (Chrome Custom Tabs on Android and Safari View Controller on iOS) for a small user flow within the app.
At the end of the process the web view should close and return to the app. I've managed to make it work on Android by using a custom scheme URL redirect at the end (myapp://home), but that doesn't seem to work on iOS. In fact, I haven't found a way to do it on iOS at all.
What approach is generally used for this type of scenario?
For iOS 11 and above, you can use SFAuthenticationSession.
Example:
let url = URL(string: "https://www.example.com")
let callbackURLScheme = "myapp"
let webSession = SFAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: { (url, error) in
//Will be triggered when a URL of scheme 'myapp' is launched
//or if the user clicks on cancel button in SFAuthenticationSession window (error case)
})
webSession.start()
Note: This is deprecated by apple
For iOS 12 and above, use ASWebAuthenticationSession. Example:
let url = URL(string: "https://www.example.com")
let callbackURLScheme = "myapp"
let webAuthSession = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme, completionHandler: { (url, error) in
//Will be triggered when a URL of scheme 'myapp' is launched
//or if the user clicks on cancel button in SFAuthenticationSession window (error case)
})
//available only in iOS 13 and above (reference: https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237232-presentationcontextprovider)
webAuthSession.presentationContextProvider = presentationContextProvider // example: UIViewController
webAuthSession.start()

How to close SFSafariViewController automatically when reaching a certain page

I want to close SFSafariViewController automatically upon reaching the "thank you" page of the Dropbox site after the user uploads something; it needs to automatically dismiss. How can I do that?
Here is what I have so far:
#IBAction func Singles5(_ sender: Any) {
let safariVC = SFSafariViewController(url: NSURL(string: "https://www.dropbox/Upload")! as URL)
self.present(safariVC, animated: true, completion: nil)
safariVC.delegate = self
}
One way I can think of is using the Custom URL scheme. You can specify your app's custom URL in the callback parameter of Dropbox (if Dropbox has callback). So when the user has finished uploading his/her file, dropbox executes the callback. In this case your app will receive the callback, with any parameters have you specified. This will call the function application(app, open, options)->Bool in your AppDelegate. Now, you can use a reference to the ViewController which presents the SFSafariViewController and call SafariViewController.dissmissViewController().

Resources