1

I'm building a basic iOS app with Xcode that mainly just contains a webview with my web app inside.

I was wondering if there was a decent way to save the users username to the devices storage when logging in so that it can be automatically entered when opening the app next time. Since the app is a webview, I don't believe there is a way to keep the user logged in (like other major apps do, such as Facebook), so I think that auto filling the username will be beneficial for them.

I found this question and answer that could possibly solve my problem, although it's in good ol' Objective C.

My current attempt, that does absolutely nothing:

let savedUsername = "testusername"

let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = " + savedUsername + ";"

self.Webview.stringByEvaluatingJavaScriptFromString(loadUsernameJS)

Is this a possibility with Swift?

Community
  • 1
  • 1
Fizzix
  • 23,679
  • 38
  • 110
  • 176

3 Answers3

5

for storing the password you should use the keychain, specifically web credentials. if done right, this will allow your app to use any existing keychain entries entered via Safari and will also allow Safari to access the password if saved via your app.

Code for setting and retrieving provided below:

private let domain = "www.youdomain.com"

func saveWebCredentials(username: String, password: String, completion: Bool -> Void) {
    SecAddSharedWebCredential(domain, username, password) { error in
        guard error == nil else { print("error saving credentials: \(error)"); return completion(false) }
        completion(true)
    }
}

func getExistingWebCredentials(completion: ((String, String)?, error: String?) -> Void) {
    SecRequestSharedWebCredential(domain, nil) { credentials, error in
        // make sure we got the credentials array back
        guard let credentials = credentials else { return completion(nil, error: String(CFErrorCopyDescription(error))) }

        // make sure there is at least one credential
        let count = CFArrayGetCount(credentials)
        guard count > 0 else { return completion(nil, error: "no credentials stored") }

        // extract the username and password from the credentials dict
        let credentialDict = unsafeBitCast(CFArrayGetValueAtIndex(credentials, 0), CFDictionaryRef.self)
        let username = CFDictionaryGetValue(credentialDict, unsafeBitCast(kSecAttrAccount, UnsafePointer.self))
        let password = CFDictionaryGetValue(credentialDict, unsafeBitCast(kSecSharedPassword, UnsafePointer.self))

        // return via completion block
        completion((String(unsafeBitCast(username, CFStringRef.self)), String(unsafeBitCast(password, CFStringRef.self))), error: nil)
    }
}

which is used like this:

// save the credentials
saveWebCredentials("hello", password: "world", completion: { success in

    // retrieve the credentials
    getExistingWebCredentials { credentials, error in
        guard let credentials = credentials else { print("Error: \(error)"); return }
        print("got username: \(credentials.0) password: \(credentials.1)")
    }
})

UPDATE

Recommend switching to using a WKWebView so you can easily pull out the response headers. Here is boilerplate code:

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let webView = WKWebView(frame: self.view.bounds)
        webView.navigationDelegate = self
        self.view.addSubview(webView)

        webView.loadRequest(NSURLRequest(URL: NSURL(string: "https://www.google.com")!))
    }

    func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
        // make sure the response is a NSHTTPURLResponse
        guard let response = navigationResponse.response as? NSHTTPURLResponse else { return decisionHandler(.Allow) }

        // get the response headers
        let headers = response.allHeaderFields
        print("got headers: \(headers)")

        // allow the request to continue
        decisionHandler(.Allow);
    }
}
Casey
  • 6,531
  • 24
  • 43
  • Looks like quite a decent solution. Although how would I trigger this upon a successful login from my web app? – Fizzix Feb 18 '16 at 23:10
  • you'd hold the username/password in memory until you get a successful login response. depending on what you are logging into will determine how you detect 'success'. ideally you'd get back JSON or XML, if not you may have to HTML scrape it – Casey Feb 18 '16 at 23:23
  • That's the thing though, I'm not to sure how I'd get that 'login response'. My web app sends the username and password to my remote API via AngularJs's `$http` function, and gets back either a successful or non successful JSON response. If successful, it redirects to the app, if not successful it displays a login error. – Fizzix Feb 18 '16 at 23:28
  • sounds like you should intercept that redirect. inside ```webView(webView: shouldStartLoadWithRequest: navigationType:)``` you can get the URL – Casey Feb 18 '16 at 23:33
  • Unfortunately, the redirect goes to the exact same URL. If the user is not logged in, my `login.php` file is included, although if the user is logged in, my `app.php` file is included. All this is within my `index.php` file, hence always being the exact same URL, just having a different PHP file included. – Fizzix Feb 18 '16 at 23:35
  • have you tried inspecting the response headers? ideally there would be a new value added when logged in. – Casey Feb 18 '16 at 23:37
  • Just compared the two, and unfortunately there's not difference. I am able to sent a response header using PHP upon successful login though. So that could be an option? – Fizzix Feb 18 '16 at 23:49
  • I have just sent a custom response header called `app-authenticated`, and I set it to `true` when logged in, and `false` when not logged in. Will this help? – Fizzix Feb 18 '16 at 23:56
  • yea, that should do it! are you seeing the "app-authenticated" in the response now? – Casey Feb 18 '16 at 23:58
  • If I inspect it with Chrome dev tools, then yes, I can see it within the response headers. – Fizzix Feb 18 '16 at 23:58
  • cool, now you just need to pull it out of the webview! here is one solution: http://stackoverflow.com/a/15476975/2066810. it'd be a cleaner if you could post the request view NSURLConnection/NSURLSession or if you switch to using WKWebView – Casey Feb 19 '16 at 00:07
  • Do you think that using a WKWebView would be a better approach than a basic webview? – Fizzix Feb 19 '16 at 00:10
  • Also, is there a way solution with Swift, instead of Objective C? – Fizzix Feb 19 '16 at 00:15
  • Awesome, now getting the headers printed to the console output. Does that mean I'll have to constantly be checking the headers to see if they change? Or can I detect a page reload with WKWebView? – Fizzix Feb 19 '16 at 00:53
  • 1
    again, not sure exactly what you are trying to achieve. but i'd assume you would constantly check the headers until "app-authenticated" is set to true. once received you'd save the username/password and then remove them from memory – Casey Feb 19 '16 at 00:59
  • Do you mind if we move this to chat for a quick moment? – Fizzix Feb 19 '16 at 01:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/103909/discussion-between-fizzix-and-casey). – Fizzix Feb 19 '16 at 01:32
1

You code is not working because you did not wrap savedUsername with quotes.

You should have this instead:

let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = \"\(savedUsername)\";"

Also, this library might help you.

Adrien Cadet
  • 1,311
  • 2
  • 15
  • 21
0

You are not passing a string to JavaScript, you should encapsulate the variable in additional quotes

let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = \"" + savedUsername + "\";"

or

let loadUsernameJS = "document.getElementById('mainLoginUsername').value = '" + savedUsername + "';"
Matija Kraljic
  • 136
  • 1
  • 4