SFAuthenticationSession completion handler not called - ios

I am trying to implement an approach to exchange cookies from Safari and App. I am using SFAuthenticationSession since the cookies sharing was disabled. I read through the topic and it seems this is the best solution to achieve this. There are not too many blogs or repos to use as an example.
I have implemented the changes redirect in the server side as following.
First I store the cookie as https://example.com/?cookie=12345. Then from the app I start an Authentication Session pointing to https://example.com/getcookie which redirects to customapp://dummy/cookies?cookie=12345
Once stated this. The swift implementation is the following (thanks to this):
let callbackUrl = "customapp://dummy/cookies"
let authURL = "https://example.com/getcookie"
self.authSession = SFAuthenticationSession(url: URL(string: authURL)!, callbackURLScheme: callbackUrl, completionHandler: { (callBack:URL?, error:Error? ) in
guard error == nil, let successURL = callBack else {
return
}
let cookie = self.parseQuery(url: (successURL.absoluteString), param: "cookie")
print(cookie!)
})
self.authSession?.start()
You may notice I am not interested on signing in but getting a cookie stored previously.
Can anyone please advice? My problem is that although the site is redirecting, the completion handler is not called, so I can't parse the callback url.
UPDATE
I found out I was missing the protocol in the Info.plist. After adding the custom protocol to it, the handler was called. Nevertheless, the handler was only called the second time I engaged the Authentication Session.
Any clue?

Related

WKWebView, get all cookies

I want obtain all cookies from WKWebView. Why? I have been started a project that use web-based auth. As result, I should intercept cookies to be sure that user is logged in and for some other purposes. Another case - imagine if user logged in, and than he "kill" the app - due to some delay in storing this cookie session will be lost :(.
The problem seems to be that the cookies are cached and not saved out
to a file immediately.
(#Kemenaran from here - p.5 below)
The point where I try to catch them -
webView:decidePolicyForNavigationResponse:decisionHandler:,
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let httpResponse = navigationResponse.response as? NSHTTPURLResponse {
if let headers = httpResponse.allHeaderFields as? [String: String], url = httpResponse.URL {
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url {
for cookie in cookies {
NSHTTPCookieStorage.shared.set(cookie)
}
}
}
}
but not all request are navigation, so one cookie (in my case) is skipped, see details below
Few words about other option I tried...
Yes, i Know that starting from iOS 11, we can use WKHTTPCookieStore as mention here. But my project should support iOS 9+
I for 100% sure, that after 5-10 sec from login, required cookie will be saved to NSHttpCookieStorage (at least all my tests during few days confirm that)
I try to use provided observer NSHTTPCookieManagerCookiesChangedNotification, but it provide me callback only for cookies that comes within webView:decidePolicyForNavigationResponse:decisionHandler
I also try to get cookies using some JS like mentioned here and also test all suggestion from here - really great article by the way. Result - negative
I also found this radar bug, and this SO question, and Sample project, but I want to prevent even this case. (described in this post applicable not only for remove but and for save) Also this situation true and when user kill the app, so case when user login, kill app and relaunch, may be present. And preventing this (simply by checking NSHttpCookieStorage for required cookies are also not good idea, because exactly after login required cookie can be stored with some delay, so this approach requires some bool-powered solution, that looks like weird..
I also read few more SO post for some related problem, and the most usefull are
This one
Another one
One more
But still without good solution...
So, is any way exist to obtain or at least force to immediately store cookies?
I ended with simple "force-like" saving Cookie from webpage.
To get all cookie i use
stringByEvaluatingJavaScriptFromString
with JS string like document.cookie();. As result i able to receive all cookies as a string with ; separator. All i need to do - parse string, create cookie and set it to NSHttpSharedStorage

iMessaged-based invitations for GameCenter for iOS 10

I'm trying to update my app to work correctly with the new features of GameCenter in iOS10.
I create a new GKGameSession on device1, get a share URL, and all that works fine. I send the share URL out via a share sheet to device 2.
Device2 clicks the link, the device briefly displays 'Retrieving...' and then launches my app. Great! But, now what? Is there context information available for this URL that I can somehow access? Otherwise I have no way how to respond when the app is launched.
Previously you'd get a callback to something adhering to the GKLocalPlayerListener protocol, to the method player:didAcceptInvite:, and you could join the match that way. But with these iCloud-based messages, the player might not be even logged into GameCenter, right? This part seems to have been glossed over in the WWDC presentation.
Also, as of today (12/28/2016) there is no Apple documentation on these new methods.
Since the GKGameSessionEventListener callback session:didAddPlayer: only fires if the game is already running, to be sure you can process this callback every time requires a work around. I've tested this and it works.
When you send out an iMessage or email invite to the game, don't include the Game Session Invite URL directly in the message. Instead use a registered URL that will open your app when opened on a device on which your app is installed. Check here to see how:
Complete Tutorial on iOS Custom URL Schemes
But add a percent escaped encoding of the game invite URL as a parameter to this URL thusly (I'm assuming the registration of a url e.g. newGameRequest but it will be best to make this quite unique, or even better - though it requires more setup, try Universal Link Support as this will allow you to direct users who don't have your app installed to a webpage with a download link)
let openOverWordForPlayerChallenge = "newGameRequest://?token="
gameState.gameSession?.getShareURL { (url, error) in
guard error == nil else { return }
// No opponent so we need to issue an invite
let encodedChallengeURL = url!.absoluteString.addingPercentEncoding(withAllowedCharacters:.urlHostAllowed)
let nestedURLString = openOverWordForPlayerChallenge + encodedChallengeURL!
let nestedURL = URL(string: nestedURLString)!
}
send the URL in a message or email or WhatsApp or whatever. Then in your app delegate, add the following:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
var success = false
if let queryString = url.query {
if let urlStringToken = queryString.removingPercentEncoding {
let token = "token="
let startIndex = urlStringToken.startIndex
let stringRange = startIndex..<urlStringToken.index(startIndex, offsetBy: token.characters.count)
let urlString = urlStringToken.replacingOccurrences(of: token, with: "", options: .literal, range: stringRange)
if let url = URL(string: urlString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
success = true
}
}
}
}
return success
}
Now you can be sure the session:didAddPlayer: will be called. What's the betting this workarround is good for about 2 weeks, and they fix this in the next release of iOS showcased at WWDC 2017 ! Update: this problem hasn't been fixed - so the workaround above remains good!
I agree, the lack of documentation is frustrating. From what I can see, we have to:
add <GKGameSessionEventListener> protocol in the class' header
Then session:didAddPlayer: fires on the joining player's device after accepting an invite link.
update:
Unfortunately, I'm not surprised to hear your results. I hadn't tried all of those scenarios, but GKTurnBasedMatch had similar shortcomings. The way I got around it there was: I added a list of player statuses to match data (invited, active, quit, etc). I gave the player a view of "pending invitations." When they opened that view, I would load all of their matches and display the entries where the player was in invited state. With GKGameSession, that should work too.
Or, it might be easier if you could maintain a local list of sessions that you are aware of. Whenever the game becomes active, pull the entire list of sessions from the server and look for a new entry. The new entry would have to be the match the player just accepted by clicking the share URL.

Authenticating a user with Instagram on iOS: specifying redirect_uri

I am developing an iOS app (using Swift) that allows the user to authenticate through Instagram using OAuth 2.0
In the past, everything was working fine as I was able to specify the authorization URL as such:
https://api.instagram.com/oauth/authorize/?client_id=xxx&redirect_uri=myiosapp://authorize&response_type=code
The key point here being the redirect_uri myiosapp://authorize
My problem is that I am no longer able to register a custom url scheme with Instagram thereby making it impossible(?) to handle the redirect exclusively through my app. If I do try to add such a URI in the "Valid redirect URIs:" field, I get the following error:
You must enter an absolute URI that starts with http:// or https://
What is the recommended way to handle authentication with Instagram exclusively thrugh an iOS native application?
After figuring it out, I thought I'd post my solution for anyone who comes across the same problem.
First of all, I'll just accept that Instagram no longer allows custom schemas in the "Security" -> "Valid redirect URIs" field. Instead, I will enter an arbitrary but valid URI that I can uniquely identify. For example: http://www.mywebsite.com/instagram_auth_ios
Now, when attempting to authorize with Instagram, I'll use that as the redirect URI - even though no webpage actually exists at that URI.
Example: https://api.instagram.com/oauth/authorize/?client_id=xxx&redirect_uri=http://www.mywebsite.com/instagram_auth_ios&response_type=code
Finally, I'll use the UIWebViewDelegate's shouldStartLoadWithRequest method to intercept the redirect request before it runs, and instead call my original custom uri (that way I don't have to rewrite anything). Here's how I wrote that method:
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
guard let url = request.URL where url.host == "www.mywebsite.com" && url.path == "/instagram_auth_ios" else { return true }
guard let authUrl = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) else { return true }
// Customize the scheme/host/path, etc. as desired for your app
authUrl.scheme = "myappschema"
authUrl.host = "instagram"
authUrl.path = ""
UIApplication.sharedApplication().openURL(authUrl.URL!)
return false
}
There's one small caveat with returning false in the shouldStartLoadWithRequest method in that it will always complain with a "Frame Load Interrupted" error. This doesn't seem to adversely affect anything and can (probably) be safely ignored.

Intercepting EVERY response with Alamofire

I'm just exploring using Alamofire and it is excellent but I'd like to do something that I feel is possible just not sure how.
Our authentication with the server uses one-time-use bearer tokens. So for every request made I have to store the new token sent down with that request.
What I'd like to do is intercept every response that comes back and check the Authorisation header. Save it to disk and then forward to the place waiting for the actual data.
Is this possible with Alamofire?
If so, please could you point me in the right direction.
Thanks
OK, after a bit of searching the github and head scratching I decided to create a new response serialiser by extending the Request type.
I created a new saveAuth() block like so...
extension Request {
public static func AuthSaver() -> ResponseSerializer<Bool, NSError> {
return ResponseSerializer { request, response, data, error in
guard error == nil else { return .Failure(error!) }
if let auth = response?.allHeaderFields["Authorization"] as? String {
Router.OAuthToken = auth // this uses a didset on the Router to save to keychain
}
return .Success(true)
}
}
public func saveAuth() -> Self {
return response(responseSerializer: Request.AuthSaver()) {_ in}
}
}
I can call it like...
Alamofire.request(Router.Search(query: query))
.validate()
.responseSaveAuth() // this line
.responseJSON {
response in
// ...
}
It still requires adding in each place that I want to strip out the newly sent auth token but it means I can choose when not to do it also and it's a single line of code.
It's maybe not the most elegant code in the extension (I'm still getting to grips with it all) but it makes it much easier to save the authentication each time.
I have solved this by only having one place in my app that sends network requests. Basically, I have a "network manager" that builds up NSURLRequests and pipes them to one function that actually sends the request (in my case it's an NSOperation sub class). That way I have only one location that I'm reading responses from.

Able to POST direct messages using Twitter's REST API, but trying to GET returns a 401 error

I am trying to get direct messages working in my app. I'm able to POST DMs just fine, but when I try to GET them from the endpoint https://api.twitter.com/1.1/direct_messages.json it returns a 401 - Unauthorized. I don't really understand how I can be authorized to send DMs but not get ones sent to me.
Here's how I'm authenticating initially:
if Twitter.sharedInstance().sessionStore.session() == nil {
Twitter.sharedInstance().logInWithCompletion { session, error in
if (session != nil) {
// successfully logged in, call loading functions
} else {
print("error: \(error!.localizedDescription)")
}
}
} else {
// already logged in, call loading functions
}
Every time I make a request using the REST API, it begins with
if let userID = Twitter.sharedInstance().sessionStore.session()?.userID {
let client = TWTRAPIClient(userID: userID)
The client is initialised the same way in both the POST and GET requests for DMs, yet the GET request fails.
As far as permissions go, I've checked that it has read/write/DM access according to Twitter, and successful requests return "x-access-level" = "read-write-directmessages";, so I think it's set properly.
I was concerned at one point that I might not be authenticating properly, since Twitter's documentation goes through the 3 step process for O-Auth and all I'm doing is telling the Twitter singleton to just log in... but I rationalised that away by assuming that those steps are all carried out in the logInWithCompletion function. And besides, if I wasn't authenticated properly I surely wouldn't be able to send DMs, right?
Any ideas on how I can fix this? I'm quite new so it may be something nice and simple! I've looked through some other questions, but they all seem to code the requests in full rather than using built-in methods like these - or have I got it all wrong?
Yeah, it was a stupid problem - I left the parameters blank since they are all marked as 'optional' - as in, a dictionary of ["" : ""]. I just set the paramaters in the request to nil, and now it works.

Resources