【Sign in with Apple】既存サービスに追加で実装するときに役立つ資料まとめ（2019年9月版）

# 概要
2019年9月12日から、新規でApp Storeに提出されるアプリかつ、ソーシャル（サードパーティ）ログインを実装しているアプリには`Sign in with Apple`の実装が必須になっているそうです。
https://developer.apple.com/news/?id=09122019b
> Starting today, new apps submitted to the App Store must follow these guidelines.

https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple
> Apps that exclusively use a third-party or social login service (such as Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon, or WeChat Login) to set up or authenticate the user’s primary account with the app must also offer Sign in with Apple as an equivalent option.

唐突すぎて信じがたいですが英文を何度読んでもそういう状況だということで、いろいろ調べてみましたのでその情報をまとめます。

背景としては、私自身が自社でiOSアプリを開発中で、既存Webサービスのアプリ化のためTwitterログインやメールログインが既に存在し、そのあたりとの連携も実装不可避になっています。
無事にリリースでき安定稼働した暁にはまた記事にまとめようと思いますが、速報として一旦現在分かっていることを記事にまとめさせていただきました。

既存サービスへの追加のため、既存ユーザーが`Sign in with Apple`に乗り換えたときのアカウントのIDを連携するようなユースケースに置かれた方々に参考になれば、と思って書きました。

# 想定読者
- 現在すでに運用しているWebサービスのアプリ版をリリースしようとしているアプリエンジニア・サーバーサイドエンジニア
- およびそれと近しい状況に置かれている方
- `Sign in with Apple`の仕組みなんもわからん、という方

# 役立った資料
## 一応公式
サーバーサイドで`Sign in with Apple`したユーザーのID連携をするときは、このあたりが公式ドキュメントになります。

https://developer.apple.com/documentation/Signinwithapplerestapi/verifying_a_user
https://developer.apple.com/documentation/Signinwithapplerestapi/authenticating_users_with_sign_in_with_apple

しかし情報がなさすぎて正直意味がわからないため、常にブラウザの固定タブで開いておきつつ、以下に示していくような非公式のWebサイトを順次巡りながら、公式に書かれていることと照らしていくような進め方を推奨します。

## 全体の流れ
https://www.slideshare.net/techblogyahoo/sign-in-with-apple-ios-idcon

バックエンドサーバで管理しているIDと、`Sign in with Apple`で得たIDをどうやって連携するかの全体的なフローがシーケンス図で書かれています。

フローの考え方としては、

**1. クライアントアプリで`Sign in with Apple`して`Authorization Code`およびID Token(JWT)を取得
2. サーバーサイドで`Authorization Code`またはID Tokenを検証し、成功すればログイン処理を行う**

となります。いくつかやり方が提示されていますので、好みというか要件に沿って選べばいいと思います。

## プライベートメールアドレスの仕組みについて

上記のスライドには、
そもそもの`Sign in with Apple`の大きな特徴である、**「ユーザーがプライベートなメールアドレスをサービス側に登録し、自身の本当のメールアドレスは隠蔽する」**という仕組みについても書かれています。

これはApple側で、「ユーザーの本当のメールアドレスとランダムに作成した偽のメールアドレスをリレーする」仕組みを用意していて、サービス側に伝えるのは偽のほうにしておくというものです。サービスから通知とかメルマガで偽のメールアドレスに送ると、Appleのメールサーバー側で転送してくれます。

何の意味がある仕組みかというと、普通、ユーザーがサービスを退会してもサービス側でユーザー情報を論理削除するか物理削除するかなんて利用規約次第なところがあって、論理削除の場合はずっとメールアドレスを保持されるためなにかの拍子に流出するリスクなどがあります。それを根本的に防ぐことができる仕組みです。

68枚目のスライドにめっちゃ重要なことが書かれていて、
**ユーザーはApple IDの管理画面からサービスとの連携を解除するというのができて、その場合はプライベートメールアドレスは変わってしまうため、再度`Sign in with Apple`したときにメールアドレスの更新処理をする必要があります。**

要は初回の`Sign in with Apple`なのか、何回目かの`Sign in with Apple`なのか、連携解除後の再度`Sign in with Apple`なのかで処理が変わってくるわけで、気をつけてテストしないと取り返しのつかないことになりそうです。

## 必要なシークレットや`client_id`の生成方法
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple

英語ですが、このサイトが非常によく書かれています。

実際にサーバーサイドから`Authorization Code`を検証するリクエストを飛ばそうと思ったら、`client_id`と呼ばれるよくわからないIDに、`client_secret`というよりよくわからないシークレットを付けて投げることになります。
https://developer.apple.com/documentation/Signinwithapplerestapi/verifying_a_user

そもそも**Appleのコンソールから`Sign in with Apple`をONにする必要があり、その中で発行されるKeyを使って`client_secret`をJWTで生成する**といった特徴的なフローがあります。
それがこの記事にはスクショ付きで一通りまとめてありますので、これに従って作業を進めればOKです。

特に`client_secret`生成が面倒で、秘密鍵やKey ID、`client_id`といったいくつかの値を使ってJWTを生成しています。上記記事ではRubyで書いてありますが、ちゃんと動きますのでこのまま利用できます。ありがたや〜

`client_secret`はいくつかの値を使ってJWTを作成したものなのですが、**有効期限が半年に設定されるのが一般的**なため、実運用にあたっては定期的に上記Rubyコードを実行する体制を整え、かつ生成したJWTをどこかセキュアなところに保存しておいてプロダクションのサーバーからその値を読み取るような仕組みが必要かと思います。私はAWS Parameter Storeを使っているので、LambdaでRubyを書いて定期実行でParameter Storeに突っ込むなどの方法を検討中です。

## 具体的なリクエストやレスポンス
https://www.slideshare.net/naohiro.fujie/idaassign-in-with-apple

実際に`Sign in with Apple`時に利用するエンドポイントを利用するときにどんなJWTが返ってきたり、どんなJWTを投げるかという踏み込んだ部分について書いてあります。

あと、私は使いませんでしたが、**Auth0**についても書いてあるので、Auth0使っている人は参考になるかもです。

## ユーザーのメールアドレスや名前を得るタイミング

上記のスライドにはハマりやすいポイントが書いてありまして、
33ページ目の**「認可レスポンスにユーザー自身のメールアドレスや名前が返ってくるのは初回のみ」**ということです。
※Webから実行したときの挙動として確認済み

そのユーザーにとって一度目または連携解除後の初回の`Sign in with Apple`時のみ、認可エンドポイントを叩いた後のレスポンスにユーザー自身のメールアドレスや名前が含まれているということです。

`Sign in with Apple`したユーザーを既存ユーザーと紐付けたいとなれば、メールアドレスで紐付けるのが定番かと思いますが、この初回タイミングを逃してしまえば連携を解除してもらわない限りできなくなってしまうので要注意です（自動で連携するのではなく、プロフィール画面などに連携するボタンがあり、押したら連携するようなUXにすれば対応可能かとは思います）。

# まとめ
まずはヤフーさんのスライドを読んで、どういった**シーケンス図**で実装しようかという目立てをします。

次に、紹介した英語の資料を読みながら、**Appleのコンソール**で秘密鍵の作成や`Sign in with Apple`をONにします。
また、**`client_secret`**の生成もやってみます。

ここまで終わったら、`Sign in with Apple`のボタンを実際にアプリやブラウザに配置して押してみましょう。

ブラウザならこちらの記事、
https://developer.apple.com/documentation/Signinwithapplejs/configuring_your_webpage_for_sign_in_with_apple

アプリならこのあたりが参考になります。
https://developer.apple.com/documentation/authenticationservices

ボタンを押してみると、ブラウザの場合はredirect_urlで指定したエンドポイントへのリクエストに、アプリからならコールバックでのイベントハンドラで`Authorization Code`やID Token、また初回リクエストの場合はメールアドレスや名前が返ってきているはずです。

次に、バックエンドでID連携処理をやるために`Authorization Code`やID Token、およびメールアドレスや名前を自前で構築したバックエンドAPIに投げます。
※CRSF対策も必要ならよしなにやってください。ID Tokenの検証の場合はただのJWT検証になるため特に必要だと思います。

バックエンドではID TokenのJWTを復号して中身のissとかsubが妥当かチェックする、または`Authorization Code`を`/auth/token`エンドポイントに投げてCodeの検証をします（補足ですが、前述の`client_secret`が必要云々は`Authorization Code`の検証をするパターンの場合のみです）。

`Authorization Code`の検証の場合は、リクエストが成功して、かつAppleへのアクセストークンなど含まれた正常なレスポンスになっていることがチェックできたら、ID連携処理を実行して、必要なアクセストークンをアプリ側に返してあげることでログイン処理完了とする、というフローがいいと思います。

# 最後に
公式ドキュメントの情報が少なすぎて錯乱しましたが、結果、かなりユーザーフレンドリーでセキュアに設計されたいい仕組みだなあと思っています（洗脳されただけかも）。

もし私と同じような状況で実装した方がいらっしゃったらぜひ情報共有しましょう。

Twitter
https://twitter.com/Meijin_garden

また、弊社では中高生が利用する勉強質問サイトを運営しています。週1〜の副業から一緒に開発するメンバーを募集中ですので、Webエンジニア、iOSエンジニアの方はいつでもお問い合わせください！
https://www.wantedly.com/companies/noschool