WebとiOSアプリでパスワードを共有する - 24/7 twenty-four seven

24/7 twenty-four seven

iOS/OS X application programing topics.

WebとiOSアプリでパスワードを共有する

iOS 8からWebサービスとアプリ間でiCloudキーチェーンを通じてパスワードなどアカウント情報を共有できるようになりました。 (ただし、現状ではiCloudキーチェーンを使えるのはSafariのみのため、MacのSafariとiOSアプリの間に限る)

昨今ではそれぞれ別のサービスで同じパスワードを再利用せず、サービスごとに固有のできればランダムなパスワードを登録して、パスワードマネージャで管理することが推奨されています。 Webサービスを使うだけならブラウザのパスワード管理などを利用すればいいのですが、そのサービスのiOSアプリを利用しようとするとパスワードを調べるのが大変でした。

iOS 8では(Safari限定ではありますが)Webサービスで入力してキーチェーンに保存したアカウント情報を、iOSアプリでも利用することができます。

パスワードなど機密性の高い情報を共有するため、どのアプリでも自由に共有できるというわけではありません。 iCloudキーチェーンをアプリから読み出すには、そのアプリがWebサービスと連携しているということを証明する必要があります。 その証明にはWebサービス側と、アプリ側の双方に手続きが必要です。

試験用に今回用意した環境の準備について

SSL

SSLはStartSSLが提供している無料の証明書を利用しました。StartSSLはアップルのTrusted Listに含まれているのでHandoffあるいはShared Credentialに使用できます。

メールサーバ

SSLの証明書を取得するのにドメインと同じホスト名のメールアドレスが必要なので、Zohoのメールサービスをを利用しました。無料です。

HTTPSサーバ

サーバはDigital OceanでUbuntuの一番安いプランを利用しました。時間単位で課金の格安VPSです。

一か月放置したとしても500円程度で済みます。1日、2日利用する程度ならほぼ無料です。

今回のケースでは単にパスワードの入力フィールドがあるSSLの静的なページがひとつあれば十分ですので、nginxにSSLの設定をして、1枚のHTMLファイルを配置するだけにしました。

DNS

メールアドレスのMXレコードを設定するためにDNSが必要なので、DOZENS を利用しました。設定できるレコード数に制限がありますが、1人で使うぶんには普通は無料の範囲で利用できます。

Webサービス側の準備について

Webサービス側に必要な手続きから説明します。

Webサービス側にはiOSアプリのIDを列挙したファイルをWebサイトのルートに決まった名前で配置する必要があります。 アプリのIDはTeam ID + Bundle Identifierです。Team IDはたいてい1ベンダーにつき1つですが、昔は複数持つこともできたので、古くからのデベロッパーの方は注意が必要です。

アプリケーションが複数ある場合は、必要なぶんだけ記入することができます。

{
    "webcredentials": {
        "apps": [ "D3KQX62K1A.com.kishikawakatsumi.Example-iOS",
                  "D3KQX62K1A.com.kishikawakatsumi.Demo-iOS" ]
    }
}

このファイルを関連付けるWebサイトのルートからアクセスできるように配置します。 これでWebサービス側の作業は完了です。

iOSアプリ側の準備について

一方iOSアプリ側では連携するドメインを列挙したEntitlementファイルを用意します。 Xcodeのターゲット>Capabilitiesのところに"Associated Domains"が追加されているのでONにします。 Entitlementファイルが無ければ自動的に追加されて、それを参照するようにビルド設定が更新されます。(すでに存在する場合は追記されます)

「+」ボタンを押して連携するドメインを「サービス:ドメイン」という形式で記述します。 サービス名は"webcredentials"と決まっています。 ドメインが複数ある場合は必要なぶんだけ記入します。サブドメインが異なる場合もキーチェーンには別ドメインとして保存されているので、必要ならすべて記入します。

f:id:KishikawaKatsumi:20150120020454p:plain

iOSアプリ側の作業は以上です。基本的にXcodeがうまくやってくれるので難しいことはありません。

iOSアプリとWebサイトの関連付けについて

Webサイト、アプリの両方の条件が整っていると、アプリケーションのインストール時、アップデート時にアプリとWebサイトの関連付けが行われます。

iOSからのリクエストに対して、Webサーバが200のステータスコードを返し、かつIDが一致し、署名が正しければ、そのアプリケーションはWebサイトと関連があるとみなされます。

ステータスコードが300〜499を返したとき、あるいはIDや署名が正しくない場合は、そのWebサイトと無関係とみなされます。

ステータスコードが500のときは一時的にサーバに障害が発生していると判断され、3時間後に再度リクエストが発行されます。

無事にアプリとWebサイトが関連していると認められれば、アプリからiCloudで同期されたSafariのキーチェーン(Shared Web Credential)を読み出すことができます。

注意事項

この関連付けの処理は、アプリケーションのインストール時、アップデート時にのみ行われます。一度関連付けが済めば、後でWebサイト側の設定を増やしたり、消したりしても、それがアプリのほうで更新されるのは再度アップデートしたときや、一度消してインストールし直したときになります。

また、こちらのクックパッド開発者ブログの記事に書かれているように、Handoffの場合と同様に、自動アップデートがあった場合はそのタイミングで一斉にアクセスがあることが予想されます。もしリクエストを処理しきれないとき、ステータスコード500を返せれば自動的に3時間後にリトライされますが、うまくレスポンスを返せなかった場合は関連付けに失敗したことになるので注意が必要です。

MacからiPhoneに遷移させよう - クックパッド開発者ブログ

iOSアプリからShared Web Credentialの利用するには

認証情報を取得する

Shared Web Credentialから認証情報を取得するにはiOS 8から新たに追加されたAPIを利用します。

func SecRequestSharedWebCredential(
                                   fqdn: CFString!,
                                   account: CFString!,
                                   completionHandler: ((CFArray!, CFError!) -> Void)!
                                  )

fqdn, accountパラメータはそれぞれオプションで指定すると、検索条件として作用します。 (例えばfqdn"kishikawakatsumi.com"と指定すると、"kishikawakatsumi.com"ドメインとして登録された情報のみ対象になります。accountパラメータも同様です。どちらか片方という使い方もできます。)

このメソッドを呼び出してShared Web Credentialから情報が取得できる場合は、OSにより自動的にユーザーに選択肢が表示されます。

f:id:KishikawaKatsumi:20150120023348p:plain

ここでユーザーはShared Web Credentialから情報を取り出すこと自体を拒否することができます。 また、複数のアカウントが存在する場合は、利用するアカウントを選択することができます。

ユーザーが拒否した場合は、コールバックには何も渡ってきません。 Webサイトとアプリが関連していれば好きなように認証情報を使えるわけではなく、あくまでもユーザーが許可した場合のみ利用できる仕組みになっています。

ユーザーがいずれかのアカウントを選択した場合、コールバックにはドメイン、アカウント、パスワードの情報が入ったDictionaryの配列が渡ってきます。 現状、複数のアカウントが存在しても、渡ってくるのはユーザーが選択したひとつだけなので、配列の形式になってはいますが、複数組の認証情報がくることは無いと思います。

アプリとWebサイトの関連づけに問題がある場合は、エラーオブジェクトの内容から問題を調べることができます。

もし、1件も認証情報が見つからなかった場合は、選択肢のUIは表示されず、即座にコールバックが呼ばれます。(そのときのエラーオブジェクトはItem not foundとなります)

無事に認証情報が取得できれば、その情報を使ってログイン処理などを行います。 成功したら認証情報をアプリのキーチェーンに保存し、次からはアプリのキーチェーンの情報を使用します。

認証情報を更新する

もし、アプリからパスワードなど認証情報の変更ができる場合、下記のメソッドを使ってアプリ側から変更をShared Web Credentialに同期することができます。

また、アプリからサインアップが可能な場合も、同じ方法でShared Web Credentialに追加しておくとSafariですぐにログインできて便利です。

func SecAddSharedWebCredential(
                               fqdn: CFString!,
                               account: CFString!,
                               password: CFString!,
                               completionHandler: ((CFError!) -> Void)!
                              )

変更の場合は、OSによって自動的にユーザーに変更の許可を求められます。 ユーザーが許可した場合はShared Web Credentialが更新されiCloudによって同期されます。 追加の場合には何も表示されません。

認証情報を削除する

もし、Shared Web Credentialの情報を削除したい場合は、passwordパラメータにnilを渡すことで削除になります。

削除の際は、変更と同様にユーザーの許可が求められます。

ただし、単にログアウトしたときなどに、削除する必要はありません。削除を行うのはユーザーがサービスを退会したときなどにすべきです。

Shared Web Credentialから情報を取得するコードは下記になります。

SecRequestSharedWebCredential(nil, nil) { (credentials, error) -> () in
    if let error = error {
        return
    }
    
    if CFArrayGetCount(credentials) > 0 {
        let credential: CFDictionary = unsafeBitCast(CFArrayGetValueAtIndex(credentials, 0), CFDictionary.self)
        let domain = CFDictionaryGetValue(credential, unsafeAddressOf(kSecAttrServer))
        let account = CFDictionaryGetValue(credential, unsafeAddressOf(kSecAttrAccount))
        let password = CFDictionaryGetValue(credential, unsafeAddressOf(kSecSharedPassword.takeUnretainedValue()))
        
        println("domain: \(unsafeBitCast(domain, CFString.self)), account: \(unsafeBitCast(account, CFString.self)), password: \(unsafeBitCast(password, CFString.self))")
    }
}

実際には、アプリのキーチェーンから認証情報を探す、見つかったらログイン。 見つからなかった場合はShared Web Credentialに問い合わせる、見つかったらログイン。 見つからなかった場合は、認証画面を表示して入力を受けつける。 入力された認証情報をキーチェーンとShared Web Credentialの両方に保存。

便利なラッパーライブラリについて

キーチェーンのAPIはなかなかに面倒なので、KeychainAccessなどラッパーライブラリを使うと簡単です。

KeychainAccessを使用すると、上記のフローは下のように書けます。

let keychain = Keychain(server: "https://www.kishikawakatsumi.com", protocolType: .HTTPS)

let username = "kishikawakatsumi@mac.com"

if let password = keychain.get(username) {
    // If found password in the Keychain,
    // then log into the server
} else {
    // If not found password in the Keychain,
    // try to read from Shared Web Credentials
    keychain.getSharedPassword(username) { (password, error) -> () in
        if let password = password {
            // If found password in the Shared Web Credentials,
            // then log into the server
            // and save the password to the Keychain

            keychain[username] = password
        } else {
            // If not found password either in the Keychain also Shared Web Credentials,
            // prompt for username and password

            // Log into server

            // If the login is successful,
            // save the credentials to both the Keychain and the Shared Web Credentials.

            keychain[username] = password
            keychain.setSharedPassword(password, account: username)
        }
    }
}

まとめ

以上、Webサービスとアプリ間で認証情報を共有できるShared Web Credentialを紹介しました。 現状ではSafari限定ということもあり、それなりの手間をかけるのに見合わないと思うかもしれません。

しかし逆説的になりますが、対応しているアプリがないがゆえに、iCloudキーチェーンを使うメリットが少なく、さらに普及が遅れるということもあると思います。

私のようにパスワードを完全にSafariの自動生成とキーチェーンで管理してるような者にとっては、ひとつでも対応アプリが増えると非常に便利に感じます。

実際、機種変更時などアプリの再ログインが必要なタイミングでパスワードを探すのが面倒で使わなくなったアプリもあります。

Shared Web Credentialを活用すると、アプリかWebのどちらかひとつでログインすればその後はパスワードの入力が不要になるというスマートな体験を提供することができます。 ぜひ、積極的に使ってみてください。

参考情報

Shared Web Credentials Reference

WWDC 2014 Session 506 - Your App, Your Website, and Safari - ASCIIwwdc