Retrieving iOS shared web credentials
The problem
One of the most annoying interactions with any app or service is logging in. None of us likes having to remember usernames and passwords, and often they are difficult to type in. Even if they are easy, it is never what you actually want to do. You are accessing an app or service in order to accomplish a task, and logging in is a barrier to success. However, none of us want to feel like our sensitive information is not secure, so logging in is something of a necessary evil.
The solution
One thing that you can do as a developer is to make this experience as simple and painless as possible. One option, which I plan to discuss in the future, is to integrate with 1Password. This is a popular password management app, and integration is quite simple.
Another approach is what I want to cover today: integrating with Shared Web Credentials, or from a user’s perspective, integrating with Safari saved passwords. This is available since iOS 8, and is an effective way to simplify life for your users. You should read the official documentation, but I will go through the steps that I did in order to retrieve passwords successfully. In a future post, I will address creating an account and saving a new password.
The steps
- Add entitlement and configure app
- Upload site association file to server
- Add password to Safari on Simulator or device
- Add code to retrieve password
- Run app
Entitlement
Xcode does a great job of simplifying the process of adding entitlements. In order to use shared web credentials, you need to enable the Associated Domains
entitlement:
Then in place of example.com
, you need to put your domain. Read through the documentation for this section to make sure that you do not miss anything.
Site association file
In order for the association to work, you have to upload a simple JSON file to your server. Even if you do not have a web app that users will sign in to, as long as you have a website for your app, you can upload the file and make it easier for your users to log in on additional devices. The file should be named apple-app-site-association
with no extension, and is simple JSON. It should look like this:
{
"webcredentials": {
"apps": [
"D3KQX62K1A.com.example.DemoApp"
]
}
}
Inside the apps
array, you should list all of the bundle identifiers for apps that should be able to share passwords from this site. The prefix to the bundle identifier is usually your Team ID. To be sure, pull up the app in developer.apple.com and copy the prefix from the App ID information.
Safari password
Since we are focusing on retrieving passwords, and not saving them from the app, it will make testing easier to manually add the password to the device you are going to test on. I will include instructions for using the Simulator, but the same basic steps apply if you are using a physical device.
- Open the Settings app
- Tap on “Safari”
- Tap on “Passwords”
- Enter
1234
for the passcode - Tap on “Add Password”
- Enter your website and a user name and password
Code
There are many approaches to retrieving the shared password. The method I have chosen is to make the call to retrieve the shared password when the user taps on the username or password field in the login form. That way, it is not as jarring for the user when first navigating to the form, but it is still helpful at the moment the user wants to take action.
The actual code for retrieving the credentials is fairly simple. Here is an example with a completion closure that returns an optional username and password.
func requestSharedPassword(completion: (username: String?, password: String?) -> ()) {
SecRequestSharedWebCredential(nil, nil) { credentials, error in
dispatch_async(dispatch_get_main_queue()) {
guard error == nil else {
completion(username: nil, password: nil)
return
}
guard let unwrappedCredentials = credentials else {
completion(username: nil, password: nil)
return
}
let arrayCredentials = unwrappedCredentials as [AnyObject]
guard let typedCredentials = arrayCredentials as? [[String: AnyObject]] else {
completion(username: nil, password: nil)
return
}
guard let credential = typedCredentials.first else {
completion(username: nil, password: nil)
return
}
guard let username = credential[String(kSecAttrAccount)] as? String,
password = credential[String(kSecSharedPassword)] as? String else {
completion(username: nil, password: nil)
return
}
completion(username: username, password: password)
}
}
}
func textFieldDidBeginEditing(textField: UITextField) {
requestSharedPassword { username, password in
guard let username = username, password = password else { return }
usernameField.text = username
passwordField.text = password
self.submit()
}
}
Execution
Finally, with the code in place, you can run the app again. When the user taps in the username or password field, call your requestSharedPassword
function, and if everything is set up properly, you will see something like this example from an app I am currently working on:
Summary
And that is it! These steps really are simple and straightforward, but the effect on the user experience of your app is tremendous.