I have a very simple web page using CakePHP 3.x and I wrote a simple Swift 4 app to display that site. As long as the app stays open sessions will continue to work. As soon as the app is closed, the user has to login again. I believe this is the correct behavior because session cookies are stored in memory and are removed once the browser is closed, but correct me if I'm wrong. My users want to log in once and just have it automatically sign in every time they reopen the app.
I'm using Swift 4 and the WKWebView. Is there a way I can store a user's credentials in the keychain for a specific URL when a user submits the login form? I found out that I can use Javascript to manipulate form fields using the WKWebView here but that's using swift 2 or 1Password. I also found this post showing how to use the keychain to store credentials. Kind of looking for a combination of the two. I also need to know what plists I need to enable for this to work.
Steps I am trying to accomplish:
The user opens the app for the first time and when WKWebView navigates to the main URL, GET "/".
CakePHP sees that the user is not authorized and redirects to the login page, GET "/users/login".
At this point, I would like to check the keychain for credentials related to this URL, but since this is the users first time to open the app the credentials should not exist. User logs in and clicks submit, POST "/users/login".
Before the form is posted, ask for permission to store credentials in the keychain.
User closes the app, clearing session.
The user opens the app, steps 1 and 2 repeat but this time on step 3 credentials do exist. At this point I would like to load the credentials from the keychain, fill the username and password field, then trigger the submit.
Here is a copy of my view controller:
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://www.example.com")!
webView.load(URLRequest(url:url))
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) {
title = webView.title
}
}
EDIT 1: As suggested in the comments, I added a feature for token based authentication by checking the request header and setting the session if the token exists. When the user signs in the first time, the token is generated and sent back in the response. I added another webView function in my ViewController to look for the token in the response header.
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
let token = (navigationResponse.response as! HTTPURLResponse).allHeaderFields["X-SAMPLE-TOKEN-HEADER"] as? String
print(token)
decisionHandler(.allow)
}
EDIT 2: Turns out adding a header to the URLRequest object isn't that hard in Swift. I have updated my viewDidLoad() to add the token header before the webView loads the request. I changed the URL to go straight to the login page. I have tested this and my user is authorized and has his session set when the header is present.
override func viewDidLoad() {
super.viewDidLoad()
var request = URLRequest(url:URL(string: "https://www.example.com/users/login")!)
request.addValue("abcdef01234567890fedcba9871634556211", forHTTPHeaderField: "X-SAMPLE-TOKEN-HEADER")
webView.load(request)
}
EDIT 3: Using this stackoverflow answer, I updated my ViewController to store the token if found in the response and to add the token in the header before the webView loads the URLRequest.
let key = "com.example.www.token"
let header = "X-SAMPLE-TOKEN-HEADER"
override func viewDidLoad() {
super.viewDidLoad()
var request = URLRequest(url:URL(string: "https://www.example.com/users/login")!)
let token = UserDefaults.standard.string(forKey: key)
if (token != nil) {
request.addValue(token!, forHTTPHeaderField: header)
}
webView.load(request)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
let token = (navigationResponse.response as! HTTPURLResponse).allHeaderFields[header] as? String
if (token != nil) {
if (UserDefaults.standard.string(forKey: key) != nil) {
UserDefaults.standard.removeObject(forKey: key)
}
UserDefaults.standard.set(token, forKey: key)
}
decisionHandler(.allow)
}
I'm open to suggestions on how to make this better but for now it meets expectations.
Using a token based authentication as suggested, tokens are generated and passed back in the response header upon user login.
WKNavigationDelegate has a method that pass in the WKNavigationResponse object, which allows you to view the response, including the headers.
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
... do stuff
let headerValue = (navigationResponse.response as! HTTPURLResponse).allHeaderFields["X-HEADER-NAME"] as? String
... do more stuff
}
The UserDefaults class can be used to store, retrieve, or delete the token.
let tokenKey = "unique.identifier"
let newToken = "NEWTOKEN12345"
let oldToken = UserDefaults.standard.string(forKey: tokenKey)
if (oldToken != nil) {
UserDefaults.standard.removeObject(forKey: tokenKey)
}
UserDefaults.standard.set(token, forKey: tokenKey)
To add a header to your initial request only, you can modify the URLRequest object to add a header before the webView loads it.
override func viewDidLoad() {
... do stuff
let tokenKey = "unique.identifier"
let token = UserDefaults.standard.string(forKey: tokenKey)
var request = URLRequest(url:URL(string: "https://www.example.com/users/login")!)
if (token != nil) {
request.addValue(token!, forHTTPHeaderField: header)
}
... do more stuff
webView.load(request)
}