1

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:

  1. The user opens the app for the first time and when WKWebView navigates to the main URL, GET "/".

  2. CakePHP sees that the user is not authorized and redirects to the login page, GET "/users/login".

  3. 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".

  4. Before the form is posted, ask for permission to store credentials in the keychain.

  5. User closes the app, clearing session.

  6. 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.

Nelson Ripoll
  • 23
  • 1
  • 8
  • 1
    Sessions (in this case PHP) are made to be ephemeral, same with cookies. It would be best to create a token system to persist your logins (IE: Upon login generate a token and save it to your server side datastore associated with the user) then save the token on the phone ( Keychain/NSUserDefaults ) Then, in your UIWebview requests, use NSMutableURLRequest to add a header with your token to authenticate on the server. – John Aug 28 '18 at 00:22
  • @John Thank you, I made changes to my backend to generate a token on login and I provide the token in the header. I added a function to my app's ViewController to look for the header in the navigationResponse. I will look at the classes you suggested next to figure out how to store the token in the keychain. – Nelson Ripoll Aug 31 '18 at 17:29

1 Answers1

1

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)
}
Nelson Ripoll
  • 23
  • 1
  • 8