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

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?

Related

Swift iOS GIDSignIn scopes are not auto selected

iOS app using GIDSignIn for o-Authentication.
We are wondering why when the scopes are presented and not auto selected
Our Client ID and app is verified in our google console and we do NOT show an unsafe app upon sign in.
Does this change when the app is live in the app store? We do not understand why we have to select the scope ourselves when in
func signInWithGoogle() {
guard let clientID = FirebaseApp.app()?.options.clientID else { return }
// Create Google Sign In configuration object.
let config = GIDConfiguration(clientID: clientID)
print("Client ID: \(clientID)")
let additionalScopes = ["https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/yt-analytics.readonly"]
// Start the sign in flow! GIDSignIn
GIDSignIn.sharedInstance.signIn(with: config, presenting: self, hint: nil, additionalScopes: additionalScopes) { [unowned self] user, error in
if let error = error {
// ...
return
}
}
GIDGoogleSignIn Framework Reference
"This is the intended behavior. We require the user to provide explicit consent for each scope being requested in addition to basic profile.
Note that you can check which scopes the user grants after a successful sign-in and, at an appropriate moment, re-request the additional scopes via
addScopes
if needed."
GoogleSignIn-iOS Github Source

How to send a message (tokens) from an iOS App to its Safari Extension?

I tried looking for a solution in posts such as this and this where people had a very similar problem: How to send a message from iOS App to a Safari Extension?
I even read this article where the author was explaining how to use SafariExtensionHandler to send a message from the browser to the app and back to the browser after selecting the context menu, but it's not quite what I was looking for.
Sending a Token from iOS App to Safari Extension
In the app, the user has to enter an email and password to log into their account. Once they log in, I save their information in UserDefaults like this:
class AuthDataService {
{...}
URLSession.shared.dataTaskPublisher(for: urlRequest)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200,
let accessToken = httpResponse.value(forHTTPHeaderField: "Access-Token"),
let clientId = httpResponse.value(forHTTPHeaderField: "Client"),
let uid = httpResponse.value(forHTTPHeaderField: "Uid")
else {
throw CustomError.cannotExecuteRequest
}
let sharedDefaults = UserDefaults(suiteName: "group.com.MyCompany.MyProject")
sharedDefaults?.set(accessToken, forKey: "Access-Token")
sharedDefaults?.set(clientId, forKey: "Client")
sharedDefaults?.set(uid, forKey: "Uid")
return data
}
{...}
}
App-Group
From my understanding of this article, I need to create an App Group, in order to share the data between the iOS App and the Safari Extension. I named the group: "group.com.MyCompany.MyProject" (just like the suiteName in UserDefaults).
Home View
The screen that the user sees when they log in, is a SwiftUI View that has a Link which takes the user to Safari so they can open the extension themselves:
struct HomeView: View {
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
Link(destination: URL(string: "https://www.apple.com/")!) {
Text("Take me to Safari")
}
}
}
SafariWebExtensionHandler
Now, all the articles that I read were talking about how to send data from the Safari Extension to the iOS app through SafariWebExtensionHandler's beginRequest(with:).
However, I'm trying to send the Tokens in UserDefaults either whenever the user logs in the app, or when they open the Safari Extension.
I tried retrieving the data from UserDefaults to see if I could at least read it in the terminal, but the debugger never gets to the print statements:
import SafariServices
import os.log
let SFExtensionMessageKey = "message"
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func readData() {
let sharedDefaults = UserDefaults(suiteName: "group.com.lever.clientTokens")
print(sharedDefaults?.object(forKey: "Access-Token")) //<-- This line never gets executed
}
func beginRequest(with context: NSExtensionContext) {
let item = context.inputItems[0] as! NSExtensionItem
let message = item.userInfo?[SFExtensionMessageKey]
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %#", message as! CVarArg)
let response = NSExtensionItem()
response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ]
readData()
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
Question
macOS vs iOS
This documentation from Apple has a section called Send messages from the app to JavaScript which is pretty much what I want to do. The documentation even mentions SFSafariApplication.dispatchMessage(withName:toExtensionWithIdentifier:userInfo:completionHandler:) which in theory sends a message to the JavaScript script, but it says it only works in macOS:
You can’t send messages from a containing iOS app to your web
extension’s JavaScript scripts.
This excellent Medium article talks about sending an APIKey from the app to the Safari Extension using an API from openai.com. It seems that it also uses SFSafariApplication to communicate with SafariWebExtensionHandler, but again it looks like it only works for macOS.
Safari Extension to webPage
I also read this other Apple documentation thinking it would help, but it only talks about passing messages from the Safari Extension's popup to the webpage.
Conclusion
So my question is:
Is writing code in SafariWebExtensionHandler the right way to send data from the iOS App to my Safari Extension? Can this be done in iOS? Or is it only available for macOS?
I read some other articles that were talking about using the JavaScript files in the Resources folder in order to "listen" to changes. But I'm a little confused as to how I can send those changes from my App in order for the Safari Extension to listen to them.
What I am trying to achieve is for the user to be already logged-in in the Safari Extension after they are redirected from the HomeView in the iOS App, instead of having to sign in another time.
Thank you for your time and help!

Session completion handler not being called for ASWebAuthenticationSession

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

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.

Login impossible with Spotify iOS 9

I can't login with ios sdk spotify.
I followed the Brian's tutorial (https://www.youtube.com/watch?v=GeO00YdJ3cE) and there is a difference with the current spotify tutorial :
In the video it talks about token exchange and show a spotify webpage. However on current spotify webpage the paragraph is missing.
Does this exchange token must be installed ?
I defined all elements in my spotify app account.
I also defined in the URL schemes : "spotify-action", "my-app-Name" but I can't sucess login.
Anyone help please ?
There are two options:
1) Implicit Grant Flow - grants users access tokens that will expire in 60 minutes - it is much simpler but has it's limits if you want to build a usable application. I will give you a simple example with spotify's updated sdk framework (you don't have to use safari)..
class ViewController: UIViewController, SPTAuthViewDelegate {
let kclientID = ""
let kcallbackURL = ""
#IBAction func loginSpotify(sender: AnyObject){
SPTAuth.defaultInstance().clientID = kclientID
SPTAuth.defaultInstance().redirectURL = NSURL(string: kcallbackURL)
SPTAuth.defaultInstance().requestedScopes = [SPTAuthStreamingScope]
SPTAuth.defaultInstance().sessionUserDefaultsKey = "SpotifySession"
SPTAuth.defaultInstance().tokenSwapURL = NSURL(string: ktokenSwapURL) //you will not need this initially, unless you want to refresh tokens
SPTAuth.defaultInstance().tokenRefreshURL = NSURL(string: ktokenRefreshServiceURL)//you will not need this unless you want to refresh tokens
spotifyAuthViewController = SPTAuthViewController.authenticationViewController()
spotifyAuthViewController.delegate = self
spotifyAuthViewController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
spotifyAuthViewController.definesPresentationContext = true
presentViewController(spotifyAuthViewController, animated: false, completion: nil)
}
func authenticationViewController(authenticationViewController: SPTAuthViewController!, didLoginWithSession session: SPTSession!) {
print("Logged In")
}
func authenticationViewController(authenticationViewController: SPTAuthViewController!, didFailToLogin error: NSError!) {
print("Failed to Log In")
print(error)
authenticationViewController.clearCookies(nil)
}
func authenticationViewControllerDidCancelLogin(authenticationViewController: SPTAuthViewController!) {
print("User Canceled Log In")
authenticationViewController.clearCookies(nil)
}
}
2) Authorization Code Flow - Spotify's authentication server sends an encrypted refresh token which you store, for example,
SPTAuth.defaultInstance().sessionUserDefaultsKey = "SpotifySession". When that sessions expires you must trigger a function on your server... Hope this helps you get started

Resources