Deleting cookies with WKHTTPCookieStore - ios

I'm using the new WKHTTPCookieStore class in order to inject and delete cookies from WKWebViews in an app.
All of the WKWebViews share a common WKWebViewConfiguration so that they can share a common cookie store.
Injecting cookies works fine using the add() method, and each of the web views can see the new cookies and send them with their requests. Deleting cookies seems to be a problem - the web views all still see the supposedly deleted cookie, and continue to send it with each request:
let cookieStore = self.webkitConfiguration.websiteDataStore.httpCookieStore
cookieStore.getAllCookies { (cookies) in
for cookie:HTTPCookie in cookies {
if cookie.name == "CookieIWantToDelete" {
cookieStore.delete(cookie, completionHandler: {
self.webView.reload() //Deleted cookie is still sent with this request
})
}
}
}
I can work around it by trashing all of the cookies in the WKWebsiteDataStore, but it seems a bit overkill.
Any ideas?

You need to clear WKWebView cache before reloading by using URLCache.shared.removeAllCachedResponses(), for exampe, or use self.webView.reloadFromOrigin() to load fresh data.

Related

How does SFAuthenticationSession store session related cookies in Safari

tl;dr
read the last paragraph.
I am using AppAuth (https://github.com/openid/AppAuth-iOS) library for handling OpenID based authentication of users for which I want to provide SSO experience through my app. The deployment target of my app is iOS 11 which means AppAuth internally uses SFAuthenticationSession. I am using authorization flow, which means the user is presented with a web based login page via SFAuthenticationSession. When a user fills in and submits the credentials SFAuthenticationSession calls completion with url (if successful) from which authorization code can be parsed. With the authorization code a token POST request through URLSession is made independently of SFAuthenticationSession and the access_token is retrieved.
The entire flow is successful including the retrieval of access_token, but when I leave the app and open user's profile webpage provided by the service provider in Safari the user is not logged in. I have tested the same flow with a Google account (https://accounts.google.com) and SSO worked fine, e.g. when I opened https://mail.google.com in Safari I was logged in. So I have a suspicion my service provider is doing something wrong. Perhaps they didn't supply me with correct scopes? But before contacting them I want to rule out any fault of mine. Now my most immediate thought is that somehow the session related cookies do not get stored in Safari. From this my question follows.
My question. Token POST request is made independently of SFAuthenticationSession (different user agent) so how any session related cookies get stored on the device (Safari) if not through SFAuthenticationSession? And is there any way to debug cookie storage in code?
According to the OAuth 2.0 standard, the token endpoint does not require the resource owner authentication, as an opposite to the authorization endpoint, which does. (A script or a back-channel performing authorization code exchange does not necessarily have access to HTTP cookies set in the user agent and, by default, browsers do not include credentials in cross-site XHRs. When a refresh token is used the resource owner interaction is not needed at all.) Your URLSession does not get any session cookie from Safari or SFAuthenticationSession and should not need one.
As for your mobile Safari experience, docs for ASWebAuthenticationSession, the SFAuthenticationSession successor, state:
All cookies, except session cookies, can be shared with Safari.
It seems to be the case for SFAuthenticationSession as well. Google must be using persistent cookies, and as a result session sharing works with them.
On a side note, even with persistent cookies there appears to be some inconsistency in syncing the cookie jars in iOS 11 environment, for example: http://www.openradar.me/radar?id=5036182937272320
To debug cookies you can use this code:
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView?
override func viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero,configuration:configuration)
self.view = webView
}
override func viewDidAppear(_ animated: Bool) {
let url = URL(string: "YOUR URL")
let request = URLRequest(url: url!)
webView?.navigationDelegate = self
webView?.addObserver(self, forKeyPath: "URL", options: [.new, .old], context: nil)
self.webView?.load(request)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let newValue = change?[.newKey] as? Int, let oldValue = change?[.oldKey] as? Int, newValue != oldValue {
print("NEW",change?[.newKey])
} else {
print("OLD",change?[.oldKey])
}
webView?.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
print(cookie)
}
}
}
}
And check if cookies has expiresDate property. If not you will not able use SSO.
If you wanted to not store the cookies in the browser, you could do auth via a private browsing session with ASWebAuthenticationSession with prefersEphemeralWebBrowserSession set to true.
https://github.com/openid/AppAuth-iOS/issues/530#issuecomment-628159766

SFAuthenticationSession completion handler not called

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?

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

WKWebView AJAX calls losing cookies

Our customer requested us to do a switch from WebView to WKWebView recently. Their app uses native login, which is done by a 2 POST calls to their backend, returning various authorization cookies that were later on used in every single HTTP/AJAX call throughout the whole app.
With the WebView, it all worked like a charm without a need to implement a single line of custom code. User logged in, cookies were stored to cookie storage by default, and WebView always took them from there and used them, since the HTTPCookieStorage was shared between NSURLSession and WebView.
It it a whole new story with WKWebView. Once we switched WebView to WKWebView, we saw that the authorization was not working. It was due to losing some cookies in the WKWebView. We store the cookies from the NSURLSession response now and append them to the WKWebView manually, by adding "Cookie" header to the HTTP requests.
We were able to get the authorization for HTTP calls work this way, but now we are seeing a new problem. Somehow, all the AJAX calls done in the WKWebView lose the authorization cookies.
Do you please know if there is any way to somehow have the authorization cookies appear in the AJAX calls too? Injecting javascript with
javascriptCookieString = #"document.cookie = 'testCookie=testValue';";
[self.webView evaluateJavaScript:javascriptCookieString completionHandler:nil];
did not work and it seems like there is no control whatsoever over Javascript calls, so I cannot alter the requests before they are being executed. Thank you.
I found that the following snippet did the trick for us. We had the same problem.
// add session cookie to ajax calls
WKUserContentController* userContentController =
WKUserContentController.new;
WKUserScript * cookieScript =
[[WKUserScript alloc]
initWithSource: [[User sharedInstance] getJavscriptCookieString]
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init];
webViewConfiguration.userContentController = userContentController;
webViewConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true;
_wk = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webViewConfiguration];
The cookie string needs to be formated correct in order to be accepted.
-(NSString*) getJavscriptCookieString {
return [NSString stringWithFormat: #"document.cookie = '%#=%#'", [self getSessionName], [self getSessionValue]];
}
Hope this can be of some help.
See also:
Can I set the cookies to be used by a WKWebView?

How can I retrieve a file using WKWebView?

There is a file (CSV) that I want to download. It is behind a login screen on a website. I wanted to show a WKWebView to allow the user to log in and then have the app download the file after they had logged in.
I've tried downloading the file outside of WKWebView after the user has logged in to the website, but the session data seems to be sandboxed because it downloads an html document with the login form instead of the desired file.
I've also tried adding a WKUserScript to the WKUserContentController object, but the script doesn't get run when a non-HTML file is loaded.
Is there a way for me to access this file while allowing users to log in via the WKWebView?
Right now, WKWebView instances will ignore any of the default networking storages (NSURLCache, NSHTTPCookieStorage, NSCredentialStorage) and also the standard networking classes you can use to customize the network requests (NSURLProtocol, etc.).
So the cookies of the WKWebView instance are not stored in the standard Cookie storage of your App, and so NSURLSession/NSURLConnection which only uses the standard Cookie storage has no access to the cookies of WKWebView (and exactly this is probably the problem you have: the „login status“ is most likely stored in a cookie, but NSURLSession/NSURLConnection won’t see the cookie).
The same is the case for the cache, for the credentials etc. WKWebView has its own private storages and therefore does not play well with the standard Cocoa networking classes.
You also can’t customize the requests (add your own custom HTTP headers, modify existing headers, etc), use your own custom URL schemes etc, because also NSURLProtocol is not supported by WKWebView.
So right now WKWebView is pretty useless for many Apps, because it does not participate with the standard networking APIs of Cocoa.
I still hope that Apple will change this until iOS 8 gets released, because otherwise WKWebView will be useless for many Apps, and we are probably stick with UIWebView a little bit longer.
So send bug reports to Apple, so Apple gets to know that these issues are serious and needs to be fixed.
Have you checked the response cookies coming back from the request. You could use a delegate method like this.
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
I've accomplished something similar to what you're trying to do:
use the web view to login as you're already doing
set a navigationDelegate on your webview
implement -webView:decidePolicyForNavigationResponse:decisionHandler in your delegate
when that delegate method is called (after the user logs in), you can inspect navigationResponse.response (cast it to NSHTTPURLResponse*) and look for a Set-Cookie header that contains the session info you'll need for authenticated sessions.
you can then download the CSV by manually specifying the Cookie header in your request with the cookies specified in the response.
Note that the delegate methods are only called for "main frame" requests. Which means that AJAX requests or inner frames will not trigger it. The whole page must refresh for this to work.
If you need to trigger behavior for AJAX requests, iframes etc you'll need to inject some javascript.
You can obtain the cookie via Javascript:
You could then use the obtained cookie to download the file manually:
webView.evaluateJavaScript("(function() { return document.cookie })()", completionHandler: { (response, error) -> Void in
let cookie = response as! String
let request = NSMutableURLRequest(URL: docURL)
request.setValue(cookie, forHTTPHeaderField: "Cookie")
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data, response, error) in
// Your CSV file will be in the response object
}).resume()
})
I do not seem to have the session cookie when I try a request from userContentController didReceiveScriptMessage.
However, when called from decidePolicyForNavigationAction, the following code detects that I'm logged in.
let urlPath: String = "<api endpoint at url at which you are logged in>"
let url = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in
let string1 = NSString(data: data, encoding: NSUTF8StringEncoding)
println(string1)
println(data)
if(error != nil) {
println("Error sending token to server")
// Print any error to the console
println(error.localizedDescription)
}
var err: NSError?
})
task.resume()

Resources