How does SFAuthenticationSession store session related cookies in Safari - ios

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

Related

How do you handle Apple Sign In with WKWebView for platform less than iOS 13?

I'm working on an app that must support Apple Sign In with iOS 10.0+. For the latest iOS I can use AuthenticationServices native library. That's fine, however, for iOS 10-12 we would like to use WKWebView to handle the authentication embedded in app to get the token and email (when possible) after the auth is complete.
I'm following their official guide from apple:
https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms
The problem now is that when I configure the redirect_uri, how do I get token back from the WKWebView after auth completes successfully? I have "tried" to intercept the response and get the token via the WKWebKit's WKNavigationDelegate navigationResponse response body but to no avail. I'm missing a key information.
public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
private func signInUsingWebAuthenticationSession() {
let queryItems = [
URLQueryItem(name: "client_id", value: "com.devapp.app"),
URLQueryItem(name: "redirect_uri", value: "https://dev.devapp.com/redirect"),
URLQueryItem(name: "response_type", value: "code id_token"), // Or code
URLQueryItem(name: "scope", value: "name email"), // Retrieve name and email
URLQueryItem(name: "response_mode", value: "form_post")
]
var urlComps = URLComponents(string: "https://appleid.apple.com/auth/authorize")!
urlComps.queryItems = queryItems
guard let authURL = urlComps.url else {
return
}
/// ... Load this url in WKWebView
}
I hope there are some smart developers out there who has encountered/solved this issue before and are happy to share their knowledge.
Cheers!
P.S If there are other solution that would work, feel free to comment
Your app will not be approved if you support iOS versions less than 13. If you are required to implement sign in with Apple ID you have to their JS solution as you have already guessed for older versions. I am too still trying to find out how
I've now completed the Apple SignIn support for iOS9 - iOS12 and would like to post the solution if other devs needed help with this. It turns out the answer is in the documentation already (link below), but some words just needs a little more explanation.
https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms
Solution:
Firstly, the 'redirect_uri' you send to Apple Auth REST API cannot be a custom scheme as only http/https is supported. To redirect auth data (incl id_token data) to your app, you need to have access to your own server, parse the response (or other logic) and perform the redirect yourself AFTER the API returns to the 'redirect_uri' you specified above. How you send the auth data to your app securely is up to you. Options are custom scheme, or redirected URL (with auth data "attached") from your server and parse the id_token out of it. I've used WKWebView and WKNavigationDelegate's decidePolicyFor delegate to capture the url info.
Once the auth data is captured in your app, you can dismiss the WKWebView. The Auth Data contains JWT. If you need the user data and email (if you requested it in the scope) etc you need to decode the JWT and look at the 'sub' and 'email'. I used JWTDecode library for decoding https://github.com/auth0/JWTDecode.swift
For iOS13+ just use the built in AuthenticationServices's ASAuthorizationController.
Cheers!

How to authenticate with Apple SSO (TV Provider) using VideoSubscriberAccount framework

I need to implement authentication scheme using Apple SSO for my application:
Check for an signed user in Settings TV Provider
Sing in into TV Provider from my app if there is no a signed user
Get authentication payload of a signed user from my provider backend with tokens, uuid, etc.
What the main steps to config and implement Apple SSO authentication using VideoSubscriberAccount framework because unfortunately there is no much info and samples about?
There are several common steps to implement authentication scheme with Apple SSO:
1. Config your provisional profile, .entitlements and Info.plist files
YourApp.entitlements file must have a special key that enables SSO for your app:
com.apple.developer.video-subscriber-single-sign-on Boolean YES
This entitlement also should be present in your provisional profile e.g.:
Info.plist must have next key with a message that will be shown to user on first access to the video subscriptions:
NSVideoSubscriberAccountUsageDescription String "This app needs access to your TV Provider."
2. Create an account manager instance and implement delegate methods to coordinate access to a subscriber's account.
import VideoSubscriberAccount
...
let accountManager = VSAccountManager()
accountManager.delegate = self
...
extension YourController : VSAccountManagerDelegate {
func accountManager(_ accountManager: VSAccountManager, present viewController: UIViewController) {
window?.rootViewController?.present(viewController, animated: true, completion: nil)
}
func accountManager(_ accountManager: VSAccountManager, dismiss viewController: UIViewController) {
viewController.dismiss(animated: true, completion: nil)
}
func accountManager(_ accountManager: VSAccountManager, shouldAuthenticateAccountProviderWithIdentifier accountProviderIdentifier: String) -> Bool {
return true
}
}
3. Determine the state of the application's access to the user's subscription information.
accountManager.checkAccessStatus(options: [VSCheckAccessOption.prompt : true]) { status, error in
...
}
If the app tries to access to subscription information first time next prompt will be shown:
4. Request information about the subscriber's account.
If access is granted you can make a metadata request to check for a signed user:
if case .granted = status {
let request = VSAccountMetadataRequest()
request.includeAccountProviderIdentifier = true
request.isInterruptionAllowed = true
accountManager.enqueue(request) { metadata, error in
...
}
}
The provider selection list and sign-in form will be shown if there is no signed account:
To skip providers list view you can set supported providers identifiers to the request e.g.:
request.supportedAccountProviderIdentifiers = ["Hulu"]
5. Second metadata request with required attributes from a identity provider's info endpoint
If there is no error and signed account is present you should call to your identity provider's info endpoint to obtain required attributes for second metadata call such as:
attributeNames: a list of SAML attributes needed
verificationToken: Base64 encoded signed authentication request from the service provider to the identity provider
channelIdentifier: service provider entity id
And make second request with these parameters:
request.attributeNames = attributeNames
request.verificationToken = verificationToken
request.channelIdentifier = channelIdentifier
accountManager.enqueue(request) { metadata, error in
...
}
6. Request translation of SAML authentication response
The seconds metadata request responds with Apple's SAML payload that should be sent to your identity provider's translation endpoint then the endpoint parses that element and returns a response that resembles the authentication payload e.g.:
if let samlPayload = metadata?.samlAttributeQueryResponse {
let body = [
...
"saml" : samlPayload
]
fetch("https://your.identity.provider/saml/translate", httpBody: body)
}
The endpoint should respond with JSON which contains all your authentication data: tokens, uuid etc.

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?

Deleting cookies with WKHTTPCookieStore

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.

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

Resources