Sign In with Apple をサーバーサイドで実装するときのポイント

iOS 13でリリースされた「[Sign In with Apple](https://developer.apple.com/jp/sign-in-with-apple/)」。iOSやmacOSなどの場合は[AuthenticationServices Framework](https://developer.apple.com/documentation/authenticationservices)を使うことで実装できますが、きちんと対応するにはWebからもログインできるようにする必要があるサービスも多いと思います。

Webで Sign In with Apple を実現するための手段として「[Sign In with Apple JS](https://developer.apple.com/documentation/signinwithapplejs)」という JavaScript SDK が Apple から提供されていますが、既存のサードパーティーログインと合わせてサーバーサイドで実装が必要なケースもあると思います。独自で実装する場合に参照するべきリソースがわかりづらかったので、ポイントと一緒にまとめます。

(2020/3/7 追記) Apple より、2020/4/30 までに、サードパーティーログインを使っているアプリについては、 Sign In with Apple に対応しなければいけないという[発表がありました](https://developer.apple.com/news/?id=03042020d)。2020年5月以降、対応していないアプリは審査に通らなくなります。

# Apple Developers で行う事前準備

実装をする前に、Apple Developers (Certificates, Identifiers & Profiles) でいくつか設定が必要になるためご紹介します。

## Register Domains and Emails for communication

「メールアドレスを隠す」設定でサインインしたユーザーに対して、メールを送信するための設定が More メニューに隠れています。メールアドレスを隠すと`abcde12345@privaterelay.appleid.com` のようなメールアドレスを取得できますが、このアドレス宛にメールを送信するには、送信元のアドレスまたはドメインがこのメニューで登録されている必要があります。

ドメイン認証の場合はそのドメイン直下に `/.well-known/apple-developer-domain-association.txt` を置くことで、メールアドレスの場合は認証メールを使って登録を行います。

なお、この項目はアプリとWebで共通のため、すでにアプリ実装時に設定している場合は再設定の必要はありません。

## Service IDs

WebサイトでSign In with Appleを実装するには、Service IDを作成する必要があります。 Identifiers メニューから作成することが可能ですが、Sign In with Appleの設定をする際に、対応するApp IDが必要となります。

ここで設定するApp IDはリリースされていなくてもかまいませんが、サインイン時に表示されるサービスのアイコンはこのApp IDに紐付いたリリース済みアプリのアイコンが使われるようなので注意が必要です（リリースされていない場合はブランクのアイコンが表示されます）。

そのほか、Service IDsのSign In with Apple設定では、設置するサイトのドメイン認証と、コールバックURLの登録を行います。HTTPのlocalhostは指定できなかったので、ローカル環境でテストを行う場合はngrokなどを使うと良さそうです。

## Keys

Service IDs の登録が終わったら、Key の作成を行います。Keys メニューから、	
Sign In with Appleのキーを作成します。ここでもApp IDの選択が必要です。

Key の登録が完了すると、 `.p8` 形式の暗号鍵ファイルをダウンロードすることができます。この鍵を後ほど使用するため、必ずダウンロードしておいてください。

# サーバーサイドで実装する際のポイント

## Authorize

Authorize エンドポイントに関する情報は [Incorporating Sign in with Apple into Other Platforms](https://developer.apple.com/documentation/signinwithapplejs/incorporating_sign_in_with_apple_into_other_platforms) に記載されています。

パラメータとして指定する内容は `client_id`, `redirect_uri`, `scope` など一般的な認証エンドポイントと同様でです。 `client_id` には、Service ID 作成時に入力した Identifier を使います。

注意すべきポイントとして、 `scope` に値を指定した場合（メールアドレスを取得する場合など）は `response_mode` に `form_post` を指定する必要があります。この値を指定すると `redirect_uri` へのコールバックが **POST形式** となりますので注意が必要です。なお、このレスポンスに `scope` で指定した内容が**初回の認証に限り**含まれていますが、Apple におけるユーザーごとに一意の値 (`sub`) の値を取得する場合は、後段の Token エンドポイントを叩く必要があります。

## Token

Token エンドポイントに関する情報は [Generate and validate tokens](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens) に記載されています。

ここで初めて `client_secret` というパラメータが登場しますが、このパラメータはドキュメント後半にも記載されている通り、JWTを指定します。つまり、Appleが発行するJWT(IdToken)を取得するために、自分がApple向けのJWTを発行する必要があるのです。

JWTの各項目に指定する内容は以下の通りです（現時点でドキュメントに記載の内容を転載します）。

|         | キー | 値 |
|:--------|:--|:--|
| header  | alg | `ES256` |
|         | kid | Apple Developer で発行したキーの Key ID (10桁) |
| payload | iss | Apple Developer で確認できる自分の Team ID (10桁) |
|         | aud | `https://appleid.apple.com` |
|         | iat | 現在時刻 (エポック秒) |
|         | exp | 現在時刻から6ヶ月以内の任意の時刻 (エポック秒) |
|         | sub | Service ID 作成時に入力した Identifier |

これらの値を使ってJWTを作成しますが、このとき発行しておいた暗号鍵を使って署名します。アルゴリズムが `ES256` と指定されていますので、ECDSA形式に対応したライブラリを使ってJWTを作成しましょう。作成したJWTを `client_secret` として使用します。

なお、 `refresh_token` を使ってトークンを再取得する際もこのエンドポイントを使用します。

(2020/3/7 追記) ここで発行した JWT は、 `exp` で指定した有効期間内であれば使い回すことが可能なため、ユーザーのログインリクエストごとに毎回発行し直す必要はありません。事前に発行したものを環境変数などに入れておくなどすることで、 JWT の発行機能をアプリケーションに組み込むことなく実装することができます。ただし、 `exp` に設定できる期間は最長で6ヶ月のため、有効期限が切れる前に確実に JWT を発行し直す、または定期的に発行し直す仕組みを整えるなどする必要があります。

### Token エンドポイントからの返却値について

Token エンドポイントからは [TokenResponse](https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse) というオブジェクトが返却されます。 `id_token` をデコードすることで `sub` を含むClaimを取得することが可能です。

Authorizeエンドポイントの際と同様に、 `email` など `scope` として指定した項目については**ユーザーごとの初回の認証に限り** ID Token の payload として含まれています。2回目以降にユーザーがログインした場合は**これらの値が含まれていない ID Token が発行される**ため、 `sub` などの値を用いてユーザーの突き合わせを行うようにしましょう。

## JWK

ID Token の署名を確認するための公開鍵については [Fetch Apple's public key for verifying token signature](https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature) にエンドポイントが記載されています。

## Profile

OAuth認証を提供するサービスの中には、Token エンドポイントで取得した Access Token を使ってユーザー情報を取得できる Profile エンドポイントが用意されている場合がありますが、 Sign In with Apple では現時点でそのようなエンドポイントは**用意されていません**（そもそもアクセストークンが払い出されません）。

ID Token 以上の情報を取得することはできませんので、ID Token の内容を使ってユーザー登録を完了させましょう。

# そのほか

## Sign In with Apple ボタン

Sign In with Apple JS を使わずに認証を実装する場合のボタン画像については、 [Incorporating Sign in with Apple into Other Platforms](https://developer.apple.com/documentation/signinwithapplejs/incorporating_sign_in_with_apple_into_other_platforms) に、パラメータを指定して画像がダウンロードできるURLが記載されています。

が、URLのパラメータをちまちまいじって画像を取得するとかやってられないので、[生成用ツールを作りました](https://app.wiwi.pw/sign-in-with-apple/)。パラメータを入力してCreateボタンを押すとボタンが生成できますので、必要に応じてご利用ください。（なぜか `border` パラメータが反映されないのですが、どなたか原因ご存知の方いらっしゃいますか……?）

Sign In with Apple に関する [Human Interface Guideline](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/) には、ここで生成したボタンの利用に関するルールが記載されています。ちなみに、ここを読んだ限りでは「公式のボタンを必ず使わなければいけない」とは記載されていない（推奨されているだけ）みたいです。いずれにしても、ユーザーがひと目で理解できるボタンを使用しましょう。

## Sign In with Apple の管理

これまでどのサービスに対して Apple ID でのサインインを行ったかについては、[Apple ID の管理画面](https://appleid.apple.com)で確認することが可能です。ここからIDの利用を解除することもできますので、初回ログインの動作を繰り返し確認したい場合などはこちらを使いましょう。

# さいごに

[REST API のドキュメント](https://developer.apple.com/documentation/signinwithapplerestapi)の Endpoint に Authorize エンドポイントに関する情報がない、`client_secret` としてJWTを生成する必要があるなど、ドキュメントや実装に若干クセのあるAPIですが、2020年3月末までに（サードパーティー製ログインを実装している場合は）既存アプリも対応が必須になっていますので、ぜひ早めに手を付けて対応しておきましょう。このページがその一助となれば幸いです。

(2020/3/7 追記) 本ページの内容をもとに、 PHPerKaigi 2020 懇親会にて飛び入りLTさせていただきました（[「Sign In with Apple のつらみ」](https://speakerdeck.com/kiwi26/tsurami-of-sign-in-with-apple)）。