Rails Backend で  Sign in with Apple の認証処理を実装する

## はじめに
とある理由により Sign in with Apple (以下、`SIWA`) の実装をしなければならなくなったのですが、[Appleの公式ドキュメント](https://developer.apple.com/sign-in-with-apple/get-started/) を読んでいても、 iOSアプリ + Railsバックエンド という構成のサービスが、一体どのようにして SIWA 機能を実装するかなどの手順や情報が乏しすぎて非常に理解に苦しんだので、OpenID Connect などの知識と組み合わせながら、実装した結果とSIWAで調べたことなどをここに残します。

## 前提
こちらの記事は、`OAuth2認証`、`OpenID Connect` の知識を前提に書いております。
僕は、以下の記事が参考になりました。
https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f
https://qiita.com/TakahikoKawasaki/items/498ca08bbfcc341691fe

アプリの実装に関しては一切触れていません。あくまでサーバ側のみです。

各種Appleでの設定に関してはこことかが参考になりそうです。
https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003

## 実装例
https://github.com/y4m4p/siwa_rails

## 認証処理の流れと全体像
1. ユーザが アプリから AppleID での認証を開始する
2. アプリが Appleから `authorization_code` と `id_token` を入手する
3. アプリが 2で手に入れた情報を サーバ側に渡す
4. サーバが `authorization_code`を Appleの認可サーバに渡す
5. Appleの認可サーバが `authorization_code`と引き換えに`id_token`などの情報を サーバに返す
6. サーバが 3で手に入れた `id_token` **を** 5で手に入れた`id_token` **で** 検証する
7. サーバが リソースへのアクセスを ユーザに付与する (※こちらは各種サービスの実装依存なので割愛)

![Untitled Diagram-Page-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333043/2c4e41c9-89f9-c17f-1fb5-12eb586a58e9.jpeg)

シーケンス図で描くと以下のようになります
![bPEzRkim38LtFmNXvTuxv0KoN2H0bonjWQ1RW82Hc5g8AmaKxRHz-kh7AfIWImTBiCbppaV1sWH1bkbjx0QZ2SxEoSczTnqgSGmlWHWmm1JtpiT6eun7Az0_efYPjjuVx5_pjF_NfpRq_hPh-xo3FV5C7DiOpHXM9rGNihe8GgxTQRmVgTgDb57CZ4A58Ysm-f-AQp3MJm9PiNi0j1eyXd1rh1jPeehF8Xff3Tj91h8nihh2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333043/6537ec29-bd27-20cd-50ea-d0ab0e87c929.png)

## SIWAの細かい話
### 認証処理の実装について
上記の 1と2 の処理において Apple Server は [`authorization_code` および `id_token`] を アプリに返却しているため、[OpenID Connect Section 3.](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) を根拠に Sign in with Apple は "[**Hybrid Flow**](https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowSteps)" を採用していることが導き出せる。

Hybrid Flow の場合、
https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowSteps
に記載されている方法でユーザの情報を取得しなければならないことが明記されている。
（以下では、SIWA仕様に読み替えて、手順を簡略化する）

1. 「クライアント」が、Appleの認可エンドポイントにエンドユーザの認証を行うためのリクエストを送る。
2. `Apple` の認可エンドポイント から「クライアント」に `authorization_code` とレスポンスタイプによって1つ以上の付加パラメータを送る。SIWA場合、付加パラメータが `id_token` や `name` である。
3. 「クライアント」が、`authorization_code` を利用して `Apple` の トークンエンドポイントにリクエストを送り、`id_token` や `access_token` が含まれるレスポンスをもらう。
4. 「クライアント」は トークンエンドポイントから返ってきた `id_token` の検証を行い エンドユーザの識別情報を得る。

ここで、「クライアント」(client)と言っているのは 「アプリ + サーバ」 であることに留意したい。
そのため、「誰が」認証エンドポイントにリクエストを投げ、「誰が」トークンエンドポイントにリクエストを投げて検証するかは、完全に実装する側に委ねられている。
[OAuth 2.0 RFC 6749 の clientの定義より](https://tools.ietf.org/html/rfc6749#section-1.1)
> client
      An application making protected resource requests on behalf of the
      resource owner and with its authorization.  The term "client" does
      not imply any particular implementation characteristics (e.g.,
      whether the application executes on a server, a desktop, or other
      devices).

1と2 (ユーザ認証処理) をアプリが行い、3と4 (認証情報の検証)に関しては サーバが行うような実装とするのが適切であろう。

サーバ側で行わなければならない検証項目は、[OpenID Connect Section 3.3.3.5.](https://openid.net/specs/openid-connect-core-1_0.html#HybridTokenResponseValidation) を根拠に特に以下の2つである。

A. `authorization_code` の検証
B. `id_token` の 検証

以下に、それぞれについて詳しく書く。

#### A. `authorization_code` の検証について
##### 検証方法
- [OpenID Connect Section 3.3.2.10.](https://openid.net/specs/openid-connect-core-1_0.html#CodeValidation)
- [OpenID Connect Section 3.3.2.11.](https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken)
 を参照してください。

##### 検証内容
- アプリからサーバに送られてきた `authorization_code` が `id_token` とペアであることを検証する。
    - この検証に失敗する場合、第三者が不正に取得した `authorization code` あるいは、`id_token` を忍び込ませている可能性がある

#### B. ID Token の検証について
##### 検証方法
- [OpenID Connect Section 3.3.3.6](https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken2)
- [OpenID Connect Section 3.3.3.7](https://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2)
を参照してください

##### 検証内容について
- 発行者(`iss`)情報が、両方の`id_token` で一致していること
- ユーザの一意な識別子(`sub`)が、両方の`id_token` で一致していること
- `nonce` 値が、両方の`id_token` で一致していること
    - All Claims about the Authentication event present in either SHOULD be present in both. の解釈より
- `email` などのユーザ情報に関する値が 両方の`id_token` で存在する場合に一致していること
    - If either ID Token contains Claims about the End-User, any that are present in both SHOULD have the same values in both. の解釈より
- `id_token` が失効していないこと
    - 失効しているトークンを受けないようにするため
- `id_token` の jwt の署名を確認すること
    - Appleの秘密鍵によって、署名が行われていることを確かめることで発行者情報を強固にする
    - 逆に署名が誤っている場合は、元のトークン情報が改ざんされている可能性がある

### SIWA と 一般的な OpenID Connect 準拠の認証方法との違いについて
丁寧にも、OpenID Connect財団の方たちが、「SIWA が 一般的なOpenID Connectの仕様と、どういう点で異なるのか」を Peculiarities の欄で全て列挙してくれている。
https://bitbucket.org/openid/connect/src/default/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md
以下、Peculiarities 欄を一部訳すると以下のような事が書かれている。
 
- Discovery document 用のエンドポイントが提供されていないので、開発者がエンドポイントやスコープ、署名アルゴリズムなどの情報を得たいと思ったときに、毎度Appleのドキュメントを精読する必要があること。本来は Discovery document を用意するのが推奨されている。

- UserInfo エンドポイントが提供されていないので、ユーザに関する情報は全て（失効する・潜在的に大きなデータになり得る）`id_token` に含まれる事になっていること。

- アプリケーションからの `scope` の指定は、「最も最初のリクエスト（すなわち連携時）」のときのみ有効になっている。もし、連携時のときに 氏名(`name`)だけの `scope` を指定してユーザが許可した場合、そのユーザの `email` は二度とアプリケーション側から取得しに行くことは出来ない。
（アプリケーションからユーザ情報の取得を行う処理の不自由さを指している）

... など、かなり独自の実装が行われている事が伺える。

## 公式ドキュメント
### Apple
- sign in with apple を利用したユーザの認証について
    - https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple
- ユーザの検証方法について
    - https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user

### OpenID Connect
- トップページ
    - https://openid.net/connect/
- 仕様
    - https://openid.net/specs/openid-connect-core-1_0.html

### OAuth 2.0
- トップページ
    - https://oauth.net/2/

## その他の参考資料
- [SIWA の ウォークスルー的なサムシング](https://blog.curtisherbert.com/so-theyve-signed-in-with-apple-now-what/)
- [id_token の validationの仕方について](https://connect2id.com/blog/how-to-validate-an-openid-connect-id-token)