[WWDC2017]パスワード自動入力機能の紹介

WWDC2018でAutoFillが強化されたので改めてiOS 11から登場したAutoFill機能について復習したいと思い、[Introducing Password AutoFill for Apps](https://developer.apple.com/videos/play/wwdc2017/206)の内容をざっくり日本語で書き出しました。WWDC2018のAutomatic Strong Passwords and Security Code AutoFillは[こちら](https://qiita.com/akatsuki174/items/04cefa5fe4439e118932)に書き起こしてあります。

# 概要

アプリ内でキーボードから直接パスワードを自動入力できるようになったのはiOS 11になってからで、すでにSafariやiCloud keychainに保存しておいたパスワードを入力できます。これによってログインが非常に簡単になります。

私は新しいアプリをダウンロードしてログインしようとした時、すでにログイン情報を入力済みのウェブサイトで確かめるためにSafariを起動した経験があります。パスワードというものは不便や、不満を感じる要因になります。確かにログインすることは重要なことですが、一方でアプリを削除することにもつながる恐れがあります。しかし私達はユーザに使ってほしいのです。こんな時に今回のパスワードの自動入力が役立つのでこれから説明します。

# Safariでのパスワード管理

iOSとmacOSにはパスワードマネージャーが組み込まれています。ウェブサイトにログインしようとした時にパスワードの保存を促されたことがあると思います。
<img width="600" alt="スクリーンショット 2018-06-16 22.32.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/72362a37-9451-7d03-3df2-b9edef9642a1.png">
ここで保存をすると次回このウェブサイトに訪れた時に自動で入力欄が埋まり、簡単にログインすることができます。もちろんセキュリティも考慮されています。あなたのウェブサイトで使われたパスワードが他で使われることはありませんし、デバイス上でパスワードを確認したかったらTouch IDかパスコードで認証する必要があります。このパスワードはiCloud keychainを通じて複数のデバイスで共有することができます。

セキュリティモデルに関しては[iOS Security Guide](https://www.apple.com/business/docs/iOS_Security_Guide.pdf)で紹介しています。ここではどのように、なぜAppleがユーザのパスワードを見ることなく複数のデバイスに同期することができるのかが説明されています。Safariの自動入力機能とiCloud keychainはiOSとmacOSに直接組み込まれており、全てのユーザがデフォルトで使えるようになっています。また、新しいデバイスをセットアップする時はiCloud keychainを有効にするように促されます。これらの機能は人気があり、多くのユーザが使っています。

iOS 11では、すでにkeychainに保存されているパスワードを用いてアプリで認証できるようになります。これからデモで見ていきます。

# iOS 11のAutoFill

ユーザ名とパスワード入力欄がある、至って標準的なログイン画面がここにあります。ユーザ名欄をタップするとキーボードのすぐ上に認証情報が表示されたQuickTypeバーが出てきます。それをタップするとすでにウェブサイトの方で入力したパスワードが自動で入力されます。あとはログインボタンをタップすればログイン完了です。最初にパスワード欄をタップした場合も同様に動きます。
<img width="600" alt="スクリーンショット 2018-06-16 23.02.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/6eb8a236-4fd2-60eb-581a-8dff360693b0.png">
パスワードセキュリティについてですが、非常に慎重に扱っています。iOS側はshiny.example.comが（デモで使った）Shinyアプリのウェブサイトだと認識してないのです。アプリとウェブサイト間に双方向の関連付けによって、正しい認証情報をQuickTypeバーに置くことができています。

iOS 11では2タップでログインできてしまうことがわかったと思います。これでもういちいちSafariに確認しに行く必要もありません。

私達は多くのアプリでPassword AutoFillの一部の機能を使えるようにしました。しかしQuickTypeバーに認証情報を表示させるには少し手を加える必要があるので、ここからはその方法について見ていきたいと思います。

# 実現方法

## QuickTypeバーを表示する

次のポイントがあります。

* 少なくとも1つはiOS端末上にパスワードが保存されていないとQuickTypeバーは表示されない
* UIKitのUITextViewとUITextFieldはパスワード自動入力機能が考慮されている
  * 独自フィールドの場合でも、UITextInputに準拠していれば対応可能
* iOSに、どのフィールドをタップした時にQuickTypeバーを表示するか教える必要がある
  * iOS 10から登場したUITextContentTypeを使用する

```swift
// UITextInputTraits Protocol
// Indicates the semantic meaning of a text-entry area
optional var textContentType: UITextContentType! { get set }
```

iOS 11では.usernameと.passwordが追加されました。これらを指定することによってパスワードの自動入力ができるようになります。UITextContentTypeはコードからでも、Interface Builderからでも設定できます。

もしあなたのアプリがユーザ名とパスワードの入力欄が異なるviewや画面にあるとしたら、特にこのUITextContentTypeは役立ちます。適切にUITextContentTypeを指定すれば両方の画面でQuickTypeバーが表示されます。

もしすでに.emailAddressを指定して実装してるなら、.usernameとemailキーボードを組み合わせるように変えれば良いです。もしパスワード欄で、自分の入力した文字列を表示する機能を付けているなら.passwordは非常に便利です。普通の、セキュアではないプレーンなフィールドでも右側に鍵アイコンの付いたQuickTypeバーを表示させることはできますが、認証情報は表示されません。

ユーザが鍵アイコンをタップするとTouch IDが発動し、それをクリアするとパスワードのリストが表示されます。この中から自分が使いたいパスワードを選択すると、その情報がアプリに入力されます。

## 認証情報が正しく入力されるようにする

知っておくべきことは次の通りです。

* 1回のタップでユーザ名とパスワードが入力される
  * たとえfirst responderをいじれないようにしてても↑このようになる
* Touch IDを起動するとアプリは非アクティブになる
  * 鍵アイコンをタップしてTouch IDを起動した時の話

ベストプラクティスとしては以下の通りです。

* 非アクティブになった時、ログインUIを引き剥がさない
  * iOSがフィールドを埋められなくなってしまう
* バリデーションには"didChange"delegateかnotificationを使うと良い
  * textプロパティから文字列を受け取る
  * 独自のフィールドの場合はUITextInputDelegateを利用すると良い

```swift
UITextFieldTextDidChange: NSNotification.Name
UITextViewTextDidChange: NSNotification.Name

protocol UITextViewDelegate { optional public func textViewDidChange(...) }
protocol UITextInputDelegate { public func textDidChange(...) }
```

## QuickTypeバーに認証情報を表示する

ここに、鍵アイコンは表示されているけど認証情報が表示されていないQuickTypeバーがあります。ユーザ体験を良くするためには認証情報が表示されている方が望ましいです。

特に何もしてないとアプリとウェブサイトを関連付けるものがありません。もしUniversalLinks対応が済んでいたら、iOSはアプリに対応したウェブサイトを見つけることができ、QuickTypeバーに認証情報を表示することができます。UniversalLinks対応をしていない場合は署名によってアプリとウェブサイトを紐づけます。iOSはアプリがインストールされるかOSがアップデートされるタイミングで、Apple-app-site-associationを探しに行きます。iOSは安全に接続されたHTTPSでこのファイルを取得しに行き、JSONのレスポンスをもらいます。このJSONのフォーマットが正しければ関連するウェブサイトと双方向リンクを持つことができます。

アプリ側での対応としては、まず適切なContentTypeを指定します。このアプリではユーザ名がアドレスの可能性があるので、今までUsernameフィールドのContentTypeをEmail Addressに指定していました。今回はAutoFill対応のためにContentTypeをUsernameに、キーボードをE-mail Addressにします。パスワードフィールドはPasswordに変更します。

これだけだとアプリとウェブサイトの紐づけができていないので関連付けを行います。XcodeのCapabilitiesタブを表示して、Associated Domainsをオンにします。Domainsの＋ボタンをタップして関連付けたいドメインを入力します。

<img width="600" alt="スクリーンショット 2018-06-17 22.47.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/24743180-6307-45a6-f2e2-8888280f73ca.png">

ドメインが追加されるとXcodeは2つのことをします。1つはこれをentitlementファイルに追加して、アプリがインストールされた時にこのドメインを元にApple-App-Site-Associationファイルを探しに行くこと、もう一つはassociated domainを有効にすることです。もしあなたのApp IDがこの機能を有効にしていない場合は、Stepsの2番目のところにチェックマークが付きません。この場合はDeveloper Portalに行ってApp IDを選択し、Associated Domainsをオンにしてください。まずCertificates, Identifiers and Profilesをクリックします。

<img width="600" alt="スクリーンショット 2018-06-17 23.43.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/ebce8041-5fb4-58af-4ba9-bfda34d977c3.png">

次にApp IDsタブから今回対応するApp IDを選択します。

<img width="600" alt="スクリーンショット 2018-06-17 23.49.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/b9e0cb1e-c8a7-6add-11d2-bcb75748c39b.png">

ここでEditボタンをクリックし…

<img width="600" alt="スクリーンショット 2018-06-17 23.51.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/5944eab5-a252-f2c4-508c-4234123207e1.png">

Associated Domainsのチェックボックスにチェックを入れます。

<img width="600" alt="スクリーンショット 2018-06-17 23.53.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/cfd0e79b-2dfa-5a08-0016-dcdb408b95e4.png">

これで完了です。

次に双方向リンクを結ぶために、Apple App Site associationファイルを作ります。一例は以下の通りです。

```json
{
    "webcredentials": {
        "apps": [ "E5336VM85F.com.example.Shiny" ]
    }
}
```

dictionaryには、ウェブサイトが関連を持っている全てのアプリのApp IDのリストを入れます。先頭はTeam Identifierで、次にBundle Identifierを記述しています。Team IdentifierはDeveloper Portalで確認することができます。

ここまでできたらこのファイルをサーバに上げます。ファイルは例えばこのようなパスに上げます。

```
https://example.com/.well-known/apple-app-site-association
https://example.com/apple-app-site-association
```

サーバのルートに上げることもできますが、well-knownディレクトリの方が優先されます。Safariでファイルを置いたアドレスでアクセスしてみてください。JSONの中身が見られれば成功です。

これで準備は整ったのでAutoFill機能を使うことができます。確認する時はデバイス上で行ってください。なぜならassociated domainはデバイス上限定だからです。

もしうまくいかなかったらswcd（Shared Web Credentials Daemon）コンソールを活用してみてください。swcdデーモンはアプリをビルドして実行するたびにアプリとウェブサイトを関連付けるための設定リクエストのログを出力してくれます。なのでうまくいかなかったらデバイスを接続し、コンソールを開いてSWCDをログを見ます。誤ってwrong.example.comをXcode内で指定してしまった場合は例えばこのようなログが出ます。

```
Request for 'https://wrong.example.com/apple-app-site-association' denied: 404/0x194 Not Found
```

ドメインを正しく修正すると例えばこのようなログが出ます。

```
Updated app ID 'E5336VM85F.ios.Shiny',
domain 'shiny.example.com',
flags 0x0 < > -> 0x2 < SiteApproved on check
```

# 保存済みのパスワードの閲覧、変更、追加

パスワード自動入力の傍らで、私達はiCloud Keychainに関連することにも変更を加えました。設定アプリの「アカウントとパスワード」はトップレベルに持ってきました。

<img width="600" alt="スクリーンショット 2018-06-18 0.24.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/89cf6bc4-3067-ed2b-8107-6197917bcb10.png">

これをタップするとTouch ID認証が求められ、認証が終わるとパスワードの一覧が見られます。新しいパスワードを追加することもできます。パスワード自動入力に対応していたらここに来る理由はないのですが、友達のデバイスでログインしたりする時はこれが役立ちます。

# サードパーティサービスに対する認証

SNSサービスのようなサードパーティのサービスにログインする機会もあるかもしれません。その際の一つの解決方法はSafariViewControllerを使うことです。SafariViewControllerにはSafariの自動入力機能が備わってますし、セキュリティも保証できます。iOS 11でSafariViewControllerは改良されました。まるでアプリの一部であるかのようにカスタマイズできるようになっています。

<img width="600" alt="スクリーンショット 2018-06-18 0.36.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/dcc8639b-8eaa-1a06-36fa-cb91ecf2408a.png">

SafariViewControllerのカスタマイズについての詳しい情報、及び実装方法については[What's New in Safari View Controller](https://developer.apple.com/videos/play/wwdc2017/225/)を御覧下さい。SafariViewControllerを用いてOAuthを実装する方法について知りたい場合はWWDC 2015の[Introducing Safari View Controller](https://developer.apple.com/videos/play/wwdc2015/504/)を御覧下さい。

# まとめ

ログイン機能はアプリを削除される危険性を伴います。私達はもちろんユーザにアプリを使ってもらいたいので、iOS 11から登場したパスワード自動入力は役立つことと思います。この機能はアプリに特に何も手を加えなくても自動的に動作します。しかし、より良い体験を提供するために次のことを行ってください。

* 現状を把握すること
* ContentTypeに.username、.passwordを指定すること
* Associated Domainsを使って紐づけをすること

先程紹介したセッションの他の関連セッションとしては[The Keys to a Better Text Input Experience](https://developer.apple.com/videos/play/wwdc2017/242/)があるので興味があればそちらも御覧ください。