WKWebView
doesn't do the "login from Facebook" job as wanted, because the browser seems to always remember the account credentials previously used to login, even though I tried to clear cookies both before and after the login requests are made.
I'm manually building a Facebook login flow for a MacOS project, following the guide here:
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
I decided to use a embedded web browser (WKWebView
) to handle the login. What I wanted to achieve is that, when user clicks on the "log in from Facebook" button, the WKWebView
gets presented as a sheet, loads the https://www.facebook.com/v3.3/dialog/oauth?client_id=[my-app-id]&redirect_uri=https://www.facebook.com/connect/login_success.html&response_type=token request, shows the dialog to let the user enter his email and password, and, after the user successfully logs in and the it gets the redirect_uri
with a token, closes itself to finish the login flow.
For the first time this procedure gets executed on a new computer, it works fine. But once I successfully login as one user, the browser seems to automatically remember the account credentials, and next time I click on "log in from Facebook", it shows and then closes immediately, which means that it directly jumps to the redirect_uri with the previous user's token appended to the uri.
I don't have much prior knowledge about how program works with caches or how it stores user credentials, but I assumed it has something to do with cookies. So I did
HTTPCookieStorage.shared.removeCookies(since: .distantPast)
and
facebookLoginRequest.httpShouldHandleCookies = false
at where I think is suitable.
These operations made a difference but still don't solve the problem. The browser doesn't close itself "right after" it's presented any more, but still, it might get the redirect-uri with the token of the previous user after 10-ish seconds and then closes itself (interrupting the ongoing credential entering behavior of the current user) and logs into my app as the previous user.
I then inspected what requests the program was loading (how it jumps between url's), and here it is:
1. https://www.facebook.com/v3.3/dialog/oauth?client_id=2390673297883518&redirect_uri=https:// www.facebook.com/connect/login_success.html&response_type=token
2. https://www.facebook.com/login.php?skip_api_login=1&api_key=2390673297883518&kid_directed_site=0&app_id=2390673297883518&signed_next=1&next=https%3A%2F%2Fwww.facebook.com%2Fv3.3%2Fdialog%2Foauth%3Fclient_id%3D2390673297883518%26redirect_uri%3Dhttps%253A%252F%252Fwww.facebook.com%252Fconnect%252Flogin_success.html%26response_type%3Dtoken%26ret%3Dlogin%26fbapp_pres%3D0%26logger_id%3Da6eb9f6e-66d4-41e3-a5f2-dc995c98183e&cancel_url=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html%3Ferror%3Daccess_denied%26error_code%3D200%26error_description%3DPermissions%2Berror%26error_reason%3Duser_denied%23_%3D_&display=page&locale=en_US
3. https://www.facebook.com/common/referer_frame.php
4. https://www.facebook.com/v3.3/dialog/oauth?client_id=2390673297883518&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&response_type=token&ret=login&fbapp_pres=0&logger_id=a6eb9f6e-66d4-41e3-a5f2-dc995c98183e
5. https://www.facebook.com/login/device-based/regular/login/?login_attempt=1&next=https%3A%2F%2Fwww.facebook.com%2Fv3.3%2Fdialog%2Foauth%3Fclient_id%3D2390673297883518%26redirect_uri%3Dhttps%253A%252F%252Fwww.facebook.com%252Fconnect%252Flogin_success.html%26response_type%3Dtoken%26ret%3Dlogin%26fbapp_pres%3D0%26logger_id%3Da6eb9f6e-66d4-41e3-a5f2-dc995c98183e&lwv=100
6. https://www.facebook.com/v3.3/dialog/oauth?client_id=2390673297883518&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&response_type=token&ret=login&fbapp_pres=0&logger_id=fa7e695e-3469-43f5-9b79-75c6130824b0&ext=1564027752&hash=AebnuofbfCPezaKn
7. https://www.facebook.com/connect/login_success.html#access_token=EAAXXXX&data_access_expiration_time=1571800153&expires_in=5104236
1, 2, 3 always take place. 4 is what's loaded when the browser interrupts you, automatically logs you as the previous user, and closes itself. It doesn't always happen, or at least it doesn't always happen after the same amount of time, so it's pretty unpredictable to me. I think it might have something to do with clearing cookies, but I don't know how.
If 4 is loaded, 5 and 6 never gets to get loaded, and we directly jump to 7 (that has the access token of the previous user), which is what should be loaded after a complete, successful login.
Then what I did was trying to block 4.
if let urlStr = navigationAction.request.url?.absoluteString {
let block = urlStr.range(of: "&ret=login&fbapp_pres=0&logger_id=")
let allow = urlStr.range(of: "&hash=")
if (block != nil && allow == nil) {
print("not ok")
print(urlStr)
decisionHandler(.cancel)
} else {
print("should ok")
print(urlStr)
decisionHandler(.allow)
}
So 4 never gets loaded. I got to complete the "entering my email and password and click the login button" action every time. But this still doesn't solve the problem, as, like a little fewer than half of all times, after I click on login, Facebook gives the following message: "Something went wrong. Try closing the browser and reopen it." Then I'll have to quit my application and reopen it, and sometimes it works fine, sometimes it still doesn't, so I'll need to quit and reopen and again.
When I click on the login button, 5 always gets loaded. But when it succeeds, 6 and 7 also gets loaded; when it fails, it gets stuck at 5, doesn't get to 6 and 7, and pops up the "something went wrong" message.
Ccreen shot of the something went wrong message:
As you can see in the screen shot, under the "something went wrong" message, my Facebook home page is also loaded. However, the loaded Facebook page doesn't necessarily belong to the user whose credential I just entered before I clicked on login but to the previous user (I tested using multiple accounts). If the loaded Facebook home page and the user who just tried to login mismatch, Facebook can detect this mismatch and then pop up a "please login first" message and force the user back to a login dialog.
This means that even though I tried to block 4, it didn't really prevent the program from logging itself in as the previous user; it probably only prevented the URL from loading and displaying in the user interface. I also searched online and it's said that the "something went wrong" message is shown when a user is already logged in but another login request is made again.
So I guess the core problem that's troubling me is still, how on earth can I get the program to stop storing the credentials of the previous user? How does this storing mechanism even work? Thanks so much in advance for anyone who would help me with solving this problem. I have poor networking knowledge and what happens here after the initial HTTP request was made looks like a big black box to me and I feel hopeless lol
Here's the entire class I've written to try to handle the login flow for reference:
import Cocoa
import WebKit
class signInWeb: NSViewController, WKNavigationDelegate {
var token = ""
var gotToken = false
static var instance : signInWeb?
var request : URLRequest?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
self.view.setFrameSize(NSSize.init(width: 1200, height: 900))
let urlString = NSString(format: NSString.init(string : "https://www.facebook.com/v3.3/dialog/oauth?client_id=%@&redirect_uri=%@&response_type=token"), Facebook.AppId, "https://www.facebook.com/connect/login_success.html") as String
let facebookUrl = URL(string: urlString)
var facebookLoginRequest = URLRequest.init(url: facebookUrl!)
self.request = facebookLoginRequest
HTTPCookieStorage.shared.removeCookies(since: .distantPast)
facebookLoginRequest.httpShouldHandleCookies = false
signInWebView.load(facebookLoginRequest)
signInWebView.navigationDelegate = self
let loadedColor = ColorGetter.getCurrentThemeColor()
backButton.font = .labelFont(ofSize: 15)
if loadedColor != ThemeColor.white {
backButton.setText(str: "Back", color: loadedColor)
} else {
backButton.setText(str: "Back", color: .black)
}
signInWeb.instance = self
}
@IBOutlet weak var signInWebView: WKWebView!
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
//HTTPCookieStorage.shared.removeCookies(since: .distantPast)
if let urlStr = navigationAction.request.url?.absoluteString {
let block = urlStr.range(of: "&ret=login&fbapp_pres=0&logger_id=")
let allow = urlStr.range(of: "&hash=")
if (block != nil && allow == nil) {
print("not ok")
print(urlStr)
decisionHandler(.cancel)
} else {
print("should ok")
print(urlStr)
decisionHandler(.allow)
}
let components = urlStr.components(separatedBy: "#access_token=")
if components.count >= 2 {
let secondHalf = components[1]
let paramComponents = secondHalf.components(separatedBy: "&")
let token = paramComponents[0]
self.token = token
gotToken = true
let parent = self.parent as! SignIn
parent.handleCollapse()
}
}
}
@IBAction func goBack(_ sender: HoverButton) {
let parent = self.parent as! SignIn
parent.handleCollapse()
}
@IBOutlet weak var backButton: HoverButton!
func changeButtonColor(color : NSColor) {
if color != ThemeColor.white {
backButton.setText(str: "Back", color: color)
} else {
backButton.setText(str: "Back", color: .black)
}
}
}
If you want to wipe all cookies, you can try this:
lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
return WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
}()
If you only want to wipe session cookies, you can try this:
lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
return WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
}()
EDIT:
I had one person demote my examples with no explanation why. But perhaps I was misleading. The second example will usually do nothing since each web-view gets its own process pool by default unless you instantiate a web-view while another one is already open (somehow apple will re-use the same process pool). What it does reveal, however, is that you may share a process pool between different web views to share session cookies between them (the opposite of clearing session cookies) and as soon as you want to clear session cookies you destroy that process pool and create a new one. So if you want to share session cookies between different web views you simply have to do the following:
extension WKProcessPool {
static var shared = WKProcessPool()
}
lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool.shared
return WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
}()
And to clear session cookies you would do:
WKProcessPool.shared = WKProcessPool()
And now next time you create a webView by the lazy example above you will have a brand new process pool and your session cookies will be wiped.