Sign in with Appleをサーバーサイドで実装した

**Sign in with Apple 実装のデッドラインは4月** ですね☺️
Web/iOS/Androidなど、複数プラットフォームをサポートするためWebViewでログイン機能を実装しているサービスや、外部サービスAPI利用の際に独自のAPIを噛ませているなど、サーバーサイドでSign in with Appleに対応するケースはあるかと思います。

iOSエンジニアであるわたしがサーバーサイドで実装した際に、わかりにくいな〜🤯と感じたポイントについて記載します。
自分のスキルセットとして、認証・認可、Webの知識はほぼ0でした🤯
**実装したソースコードをの抜粋を載せていますが、言語はrubyです。**

# やること
今回ご説明する Sign in with Apple の全体のざっくりフローは、

1. Apple Developer サイトでの設定
2. Apple ID サインイン画面表示（Authorizeエンドポイント）、認可コード受取り
4. 認可コードを使い、AppleのユーザーIDを取得（Tokenエンドポイント）

です。

**取得したいデータがemailとnameのみの場合 → ステップ２まで**
**ユーザーIDを取得したい場合 → ステップ3まで**
実装する必要があります。

# 参考までに前知識
### Auth関連
まず最初に、OAuth周りの知識があると理解が早いと思います。
この方のqiitaが非常に参考になりました。（ありがとうございました。）

* [一番分かりやすい OAuth の説明
](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)
* [一番分かりやすい OpenID Connect の説明
](https://qiita.com/TakahikoKawasaki/items/498ca08bbfcc341691fe)

### Appleガイドライン関連

* [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/)
* [アプリ審査ガイドライン](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple)
* [Webサイトおよびその他のプラットフォームの使用ガイドライン](https://developer.apple.com/sign-in-with-apple/usage-guidelines-for-websites-and-other-platforms/)

# Apple Developer サイトでのもろもろ
ではさっそくApple Developerサイトを開きます。

### Servise ID作成からドメイン認証

1. 対象の App ID を作成し、Capabilities として Sign in with Apple を追加する
2. Apple Developer の Certificates, Identifiers & Profiles から Service ID を追加する
3. Primary App ID を 1 のAppに指定する
4. Domainsに認可コードのリダイレクト先のドメインと Return URLs を指定する
5. Domainの検証ファイル `apple-developer-domain-association.txt` をダウンロードする
6. Domainの検証ファイルをWebサイトの `https://.../.well-known/` ディレクトリに置く

**Service ID** と **Return URLs** はAuthorize、Tokenエンドポイントで利用します。

### ドメイン検証に失敗したら

* `https://{yourdomain}/.well-known/apple-developer-domain-association.txt` にアクセスできるか
* 検証ファイルは有効期限内か（7日間のみ有効）
* 検証ファイルのダウンロードは何回でも可能なので、常に最新のファイルを使う
* サーバーがTLSの条件を満たしているか

を確認する。

明文化されていなさそうですが、Appleさんからサポートを受けた開発者がフォーラムに書き込んでいます。
https://forums.developer.apple.com/thread/122124

そのほか、ネットワーク構成などに問題がないか等、公式の [Troubleshooting Domain Verification](https://help.apple.com/developer-account/?lang=en#/dev1c0e25352) を読んでみるとよいです。

### Key をダウンロード
Tokenエンドポイントのリクエストパラメータの **JSON Web Token** 作成に使います。
当然ですが、**センシティブなファイルなので暗号化などよしなに行い、サーバーのどこか安全なところに置いて**おく。
今回はわたしは、`.txt`に変換して使いました。

公式ドキュメントは [Create a Sign in with Apple private key](https://help.apple.com/developer-account/?lang=en#/dev77c875b7e)

# Apple IDサインインページ表示

## Appleサインインボタン

公式ドキュメント [Incorporating Sign in with Apple into Other Platforms](https://developer.apple.com/documentation/signinwithapplejs/incorporating_sign_in_with_apple_into_other_platforms) の *Add a Custom “Sign in with Apple” Button* にダウンロードURLやサイズ、カラー指定のクエリパラメータの説明があります。
[Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/) にしたがって正しくつかいましょう。

## Authorizeエンドポイント
Appleの認証ページURLのクエリパラメータ、レスポンスについては [Incorporating Sign in with Apple into Other Platforms](https://developer.apple.com/documentation/signinwithapplejs/incorporating_sign_in_with_apple_into_other_platforms) に記載があります。

大事なのは、

```
client_id = Service IDで指定した文字列
redirect_uri = Service IDに設定したリダイレクトURI
```

です。

* `response_type` = `id_token`にする場合、`response_mode`は`fragment`または`form_post`である必要がある
* 氏名やメールアドレスを取得したい（`scope`を指定する）場合、`response_mode` = `form_post`にする必要がある

など、諸々の制約があります。

サインインページが表示されると、ユーザーによるApple IDの認証を受け付けます。
認証が完了すると、指定したリダイレクトURIにリダイレクトが行われます。
受け取れる値は認可コードと、`scope`に指定がある場合**初回1度のみ**ユーザーID以外の個人情報（メールアドレス、氏名など）が受け取れます。

## サインインページがうまく表示されない
**invalid_uri** や **invalid_client** が表示されたら、焦らず `client_id` と `redirect_uri` を今一度確認しましょう。
開発・本番など環境別でドメインを分けている場合、 `redirect_uri` が変わると思いますので要チェックです。
Apple Developer 上で作成した Service ID の **Returns URLs** に該当のリダイレクト先が登録されているか確認しましょう。

# アクセストークン・ユーザーID取得

## Tokenエンドポイント
Appleのトークン発行エンドポイントにリクエストを行い、トークンの発行を行います。
パラメータ、レスポンスについては [Generate and validate tokens](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens) に記載があります。
パラメータは **Authorization** のものを使います。

### `client_secret`の値のJSON Web Token(JWT)作成
Apple Developers で発行した**Key**を用いて、リクエストパラメータ `client_secret` に付与するJWTの作成を行います。
JWTとは、 `header` と `payload` から成るJSON構造をエンコードし電子署名をつけたものです。
デコード&エンコードしたJWTの検証は、 [https://jwt.io/](https://jwt.io/) がおすすめです。

rubyでのJWTのエンコード&署名の例は以下です。
[jwt/ruby-jwt](https://github.com/jwt/ruby-jwt) というGemを利用しています。

`kid` や `iss`、`sub`などは暗号化しておくと良いでしょう。

```ruby
require 'jwt'
require 'openssl'

  # 署名用の鍵を取得
  def pem
    file = "path/to/key.txt" # Apple Developerで発行したKeyファイル
    # key.txtを読み出し、暗号鍵を生成する
    OpenSSL::PKey::EC.new(File.read(file))
  end

  # 署名付きIDトークンを取得
  def id_token
    header = {
      'kid' => 'Apple Developerで発行したKey ID'
    }
    claim = {
      'iss' => 'Apple DeveloperでのTeam ID',
      'iat' => Time.now.to_i,
      'exp' => Time.now.to_i + 15777000,
      'aud' => 'https://appleid.apple.com',
      'sub' => 'Apple Developerで発行したService ID'
    }
    JWT.encode(
      claim,
      pem,
     'ES256',
      header
    )
  end
```


### レスポンスの検証
レスポンスに含まれるIDトークンの検証には、Appleの公開鍵取得が必要です。
エンドポイントについては [Fetch Apple's public key for verifying token signature](https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature
) に記載があります。

[jwt/ruby-jwt](https://github.com/jwt/ruby-jwt) を用いた検証の実装例です。

```ruby
  # IDトークンをデコードし署名を検証する
  # public_keysに渡すのは、https://appleid.apple.com/auth/keys の "keys"の値です。
  def decode(id_token, public_keys)
    JWT.decode(
      id_token,
      nil,
      true,
      { algorithms: 'E256', jwks: { keys: public_keys } }
    )
  end
```
### IDなどのユーザー情報
`sub`の値がユーザーの一意の識別子（ユーザーID）です。
そのほかのレスポンスのIDトークンに含まれる情報については [Retrieve the User’s Information from Apple ID Servers
](https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple) に記載があります。
あとは取得したデータをもとに自サービスでのユーザー認証を完了させましょう。

# おしまい
書くと簡単ですが実際はたいへんだったので**iOS boys and girlsの皆様はできる限りネイティブでやりましょう:tada:**

# Thanks to
https://qiita.com/kiwi26/items/5b8cc53ed8d10a403f00
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple

以上です🎉🎉🎉
ありがとうございました😽