Shared Web Credentials

Shared web credentials is a programming interface that enables native iOS apps to share credentials with their website counterparts. For example, a user may log in to a website in Safari, entering a user name and password, and save those credentials using the iCloud Keychain. Later, the user may run a native app from the same developer, and instead of the app requiring the user to reenter a user name and password, shared web credentials gives it access to the credentials that were entered earlier in Safari. The user can also create new accounts, update passwords, or delete her account from within the app. These changes are then saved and used by Safari.

Overview

The shared web credentials API defines Core Foundation–based functions for storing and requesting shared password-based credentials. There are four steps to sharing credentials:

  1. Add a com.apple.developer.associated-domains entitlement to your app. This entitlement must include all the domains with which you want to share credentials.

  2. Add an apple-app-site-association file to your website. This file must include application identifiers for all the apps with which the site wants to share credentials.

  3. When the app is installed, the system downloads and verifies the site association file for each of its associated domains. If the verification is successful, the app is associated with the domain.

  4. An app can share credentials with any associated domains by calling SecAddSharedWebCredential(_:_:_:_:) and SecRequestSharedWebCredential(_:_:_:).

Adding the Associated Domains Entitlement

To enable shared credentials in your app, add the com.apple.developer.associated-domains key to your app’s entitlements. You can add this entitlement using the target’s capabilities pane (see Figure 1).

Include a list of all the domains with which your app wants to share credentials. Keep the number of domains to about 20 to 30. To match all subdomains of an associated domain, you can specify a wildcard by prefixing *. before the beginning of a specific domain (the period is required).

Each domain you specify uses the following format:

<service>:<fully qualified domain>[:port number]

For shared web credentials, always use the webcredentials service.

Figure 1

Adding the associated domains for your app

Adding the Site Association File

Place your apple-app-site-association file on a website that matches one of the domains in your app’s associated domains entitlement (you can also place it in the .well-known subdirectory). The file’s URL should match the following format:

https://<fully qualified domain>/apple-app-site-association

The file’s JSON content must contain a webcredentials dictionary with an apps array. The apps array lists the application identifiers for all the apps that can access the shared credentials for this site. For an example, see Listing 1.

Listing 1

Server-side web credentials

{
   "webcredentials": {
       "apps": [    "D3KQX62K1A.com.example.DemoApp",
                    "D3KQX62K1A.com.example.DemoAdminApp" ]
    }
}

The apple-app-site-association file must meet the following requirements:

  • The file must be hosted on an https:// site with a valid certificate (for example, Safari must not issue a certificate warning when viewing the site).

  • The file must not use any redirects.

  • In iOS 9.3.1 and later, the file must be no larger than 128 KB (uncompressed), regardless of whether it is signed.

  • If your app runs in iOS 9 and later and you use HTTPS to serve the file, you can create a plain text file that uses the application/json MIME type and you don’t need to sign it.

    If your app runs in iOS 8, the file must have the MIME type application/pkcs7-mime and it must be CMS signed by a valid TLS certificate.

You can sign the JSON file using Terminal commands such as those shown in Listing 2. The echo command puts the server-side web credentials from Listing 2 into a file named json.txt, removing most of the white space from the text to minimize the required bandwidth. The line beginning with the cat command reads the file and pipes the output to the openssl command, which signs the file using the certificate and key for an identity valid for example.com.

Listing 2

Signing the credentials file

echo '{"webcredentials":{"apps":["D3KQX62K1A.com.example.DemoApp",
                           "D3KQX62K1A.com.example.DemoAdminApp"]}}' > json.txt
 
cat json.txt | openssl smime -sign -inkey example.com.key
                            -signer example.com.cert
                            -certfile intermediate.cert
                            -noattr -nodetach
                            -outform DER > apple-app-site-association

The output of the openssl command is the signed JSON file that you put on your website at the apple-app-site-association URL, in this example https://example.com/apple-app-site-association.

The certificate used to sign the JSON file must meet the following requirements:

  • The certificate must be a valid TLS certificate for the domain name. For example, the certificate must have a Common Name or Subject Alternative Name that matches the domain (example.com in the example).

  • The certificate must not be expired.

  • The server’s certificate chain must lead to a root certificate issued by a certificate authority (CA) trusted by iOS.

For the complete list of certificate authorities trusted by iOS, see the Apple support article Lists of available trusted root certificates in iOS.

Validating the Site Association File

When the app is installed, iOS attempts to associate it with all the domains listed in its associated domains entitlement. iOS performs the following steps:

  1. Takes each domain from the associated domains element.

  2. Appends “/apple-app-site-association” to the end of the domain, and downloads the contents at the resulting URL.

  3. Verifies the JSON file’s signature.

  4. Makes sure the JSON file includes the app’s application identifier.

If all of these steps succeed, the app is associated with the domain and you can use SecAddSharedWebCredential(_:_:_:_:) and SecRequestSharedWebCredential(_:_:_:) to access and modify the shared web credentials. After an app is successfully associated with a domain, it remains associated until the app is deleted from the device.

A number of issues might cause the validation to fail.

  • The JSON signature is invalid. The association is denied.

  • JSON file is invalid or does not contain the application identifier. The association is denied.

  • The server returns a 300-499 code. This includes redirects. The association is denied.

  • The server returns a 500-599 code. The system assumes that the file is temporarily unavailable and tries again. By default, the system tries every 3 hours for up to eight tries. It also tries whenever the app calls the SecAddSharedWebCredential(_:_:_:_:) or SecRequestSharedWebCredential(_:_:_:) method.

Sharing Credentials

There are a wide range of common situations where you may want to use shared web credentials. This section covers four: logging in to a remote server, creating a user account in the app, changing a user’s password, and deleting a user’s account.

Logging In to a Remote Server

Typically, when an app needs to log in to a remote service, you start by checking for the user’s credentials in the iOS keychain. If you have current credentials for the user, you can log them in directly. If not, prompt the users for their user name and password, and then try to log them in. You will also want to save their credentials after the login is successful. This workflow is shown in Figure 2.

Figure 2

Basic login workflow

When using shared web credentials, you add two steps to this procedure. As before, you start by checking if the user’s credentials are stored in the keychain. If you cannot find the user’s credentials, you check for shared web credentials. If you still cannot find any credentials, or if the user declines to use the shared credentials, you must prompt the user for her name and password. Try to log the user in, and if the login is successful, save the credentials to both the keychain and the shared web credentials. This ensures that the user has access to the credentials in Safari as well as within your app. This workflow is shown in Figure 3.

Figure 3

Login workflow with shared web credentials

Do not use the shared web credentials as your primary storage for secure user credentials. Instead, save the user’s credentials in the keychain, and only use the shared web credentials when you can’t find the login credentials in the keychain.

To read the user’s credentials from the shared web credentials, use the SecRequestSharedWebCredential(_:_:_:) function as shown in Listing 3.

Listing 3

Accessing shared web credentials

SecRequestSharedWebCredential(NULL, NULL, ^(CFArrayRef credentials, CFErrorRef error) {
 
    if (error != NULL) {
        // If an error occurs, handle the error here.
        [self handleError:error];
        return;
    }
 
    BOOL success = NO;
    CFStringRef server = NULL;
    CFStringRef userName = NULL;
    CFStringRef password = NULL;
 
    // If credentials are found, use them.
    if (CFArrayGetCount(credentials) > 0) {
 
        // There will only ever be one credential dictionary
        CFDictionaryRef credentialDict =
        CFArrayGetValueAtIndex(credentials, 0);
 
        server = CFDictionaryGetValue(credentialDict, kSecAttrServer);
        userName = CFDictionaryGetValue(credentialDict, kSecAttrAccount);
        password = CFDictionaryGetValue(credentialDict, kSecSharedPassword);
 
        // Attempt to log in
        success = [self logIntoServer:server user:userName password:password];
    }
 
    if (!success) {
        [self promptForUserNameAndPassword];
    } else {
        [self saveCredentialsToKeychainForServer: server user:userName password:password];
    }
});

Creating a User Account in the App

If the user can create new accounts in your app, you should save the user name and password to the shared web credentials. In this way, the user can easily access the account from Safari, as well as from within your app. You can save the user’s name and password to the shared web credentials using the SecAddSharedWebCredential(_:_:_:_:) function as shown.

Listing 4

Creating a user account

SecAddSharedWebCredential(domain, name, password, ^(CFErrorRef error) {
 
    if (error != NULL) {
        // Handle the error here...
        return;
    }
 
    // The credentials have been successfully saved.
 
});

If there are no existing credentials for this user name and domain, this method completes without prompting the user for permission.

Changing a User’s Password

If the user changes her password in the app, you must update both the credentials stored in the keychain and in the shared web credentials. You can change a password using the SecAddSharedWebCredential(_:_:_:_:) function. This is exactly the same procedure used when creating a new user account (see Listing 4). However, if credentials already exist for the given user name and domain, this method prompts the user for permission before making the change. The user can cancel this change.

Deleting a User’s Account

If the user deletes her account, you should remove the credentials from both the keychain and the shared web credentials. You can remove the credentials for a given domain and user name by calling the SecAddSharedWebCredential(_:_:_:_:) function and passing NULL for the password.

Listing 5

Deleting a user account

SecAddSharedWebCredential(domain, name, NULL, ^(CFErrorRef error) {
 
    if (error != NULL) {
        // Handle the error here...
        return;
    }
 
    // The account information has been successfully deleted.
 
});

The system prompts the user for permission before deleting their user name and password from the shared web credentials. Users can cancel this change.

Symbols

Managing Shared Passwords

func SecAddSharedWebCredential(CFString, CFString, CFString?, (CFError?) -> Void)

Asynchronously stores (or updates) a shared password for a website.

func SecRequestSharedWebCredential(CFString?, CFString?, (CFArray?, CFError?) -> Void)

Asynchronously obtains one or more shared passwords for a website.

func SecCreateSharedWebCredentialPassword()

Returns a randomly generated password.

Constants

kSecSharedPassword

A dictionary key whose value is a shared password.