Sign in with Apple: 2022年6月のアカウント削除機能に対する要件の適応に対応した話（リフレッシュトークンの取得とトークンの無効化）

今日はSign in with Appleにおける、サーバー側でのリフレッシュトークンの取得と、トークンの無効化処理について書きます。

これは2022年6月から適応される新しいガイドラインにて、アカウント削除機能に対する要件が新しく定義され、それに対応するためのものです。

<!-- more -->


## 背景

Appleから[アカウント削除時の必須要件](https://developer.apple.com/news/?id=12m75xbj)に関するお知らせがあり、2022年6月30日から、新しいレビューガイドラインが適応されることになっています。

そこでは、

> If your app offers Sign in with Apple, you’ll need to use the Sign in with Apple REST API to revoke user tokens when deleting an account.

や

> ** My app uses Sign in with Apple to provide account creation and authentication to users. What changes are necessary to support users who delete their accounts?**
>  Apps that support Sign in with Apple should use the Sign in with Apple REST API to revoke user tokens. To learn more, review the documentation and design recommendations.


と書かれており、Sign in with Apple(AppleIDでのサインイン)を可能にしている場合は、ユーザーのアカウント削除時に、Appleの提供するAPIを使ってユーザーのトークンを無効化する必要があります。

私の方では、iOSアプリについてSign in with Appleでのサインインを提供していましたが、トークンの無効化処理を行なっていませんでした。そこで今回、トークンの無効化処理を加えました。

本記事では、トークンの無効化処理に必要なリフレッシュトークンの取得方法と、トークンの無効化処理について説明します。

![スクリーンショット 2022-06-14 10.38.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627146/72ee0029-1150-7863-663b-3381b6b575a3.png)



## 今までのアカウント処理の流れ(改修前)

今までは下記のフローでログイン系の処理を行なっていました。

全体としては、iOSアプリ側で認証を行い、サーバーでユーザー情報を検証・作成・提供するという流れです。

### サインアップ時

1. アプリ: AppleIDでサインアップする。

2. アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。

3. サーバー:　アプリから受け取ったJWTを検証し、必要な情報を保存しておく。

## サインイン時

1. アプリ: AppleIDでサインインする。

2. アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。

3. サーバー: アプリから受け取ったJWTを検証し、一致するユーザーをアプリに返す。

## アカウント削除時

1. サーバー: 指定されたアカウントの情報を削除する。

## 今回改修したアカウント処理の流れ(改修後)

以下の2点を変更しました。

1. サインアップ時にリフレッシュトークンを取得し、サーバーに保存する

2. アカウント削除時に、APIによりトークンを無効化する

[トークンを無効化するAPI](https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens)では、リフレッシュトークンもしくはアクセストークンが必要です。一般的にサインアップからアカウント削除までには長時間が開くため、長期的に有効であるリフレッシュトークンを保持するようにしました。

### 事前準備

1. (今回付け加えたところ) Apple Developerでキーを発行しておく。これはサーバー側でトークンの発行のリクエストに必要なJWTの生成に必要になる。

### サインアップ時

1. アプリ: AppleIDでサインアップする。

2. アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。

3. サーバー:　アプリから受け取ったJWTを検証し、必要な情報を保存しておく。

4. (今回付け加えたところ) サーバー: アプリから受け取った認証コードを元にAPIを元にリフレッシュトークンを取得し、保存しておく。

## サインイン時

1. アプリ: AppleIDでサインインする。

2. アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。

3. サーバー: アプリから受け取ったJWTを検証し、一致するユーザーをアプリに返す。

### アカウント削除時

1. (今回付け加えたところ) サーバー:　サインアップ時に保存していたリフレッシュトークンを使い、Appleの無効化処理APIを叩いてトークンを無効化する。


## リフレッシュトークンの取得とそれを利用したトークンの無効化処理

ではここから本題の、今回加えたサインアップ時のリフレッシュトークンの取得と、アカウント削除しのトークンの無効化処理の方法について記載します。

## 1. Apple Developerからキーを作成する

サーバーからアプリのサーバーにAPIリクエストを送る時に必要なキーを生成します。

Developer -> 「Certificates, Identifiers & Profiles」 -> Keysから作成します。

キーのタイプに「Sign in with Apple」を選択し、AppIDを指定します。

キーが生成されるので、ダウンロードして、必要な場所に保存します。

（ピリカではGCPのSecretManagerを利用しており、そこにアップロードしました。）


## 2. アプリ側の認証時にauthorizationCodeを取得しサーバーに送信する。

アプリでは、AuthenticationServicesフレームワーク([参考](https://developer.apple.com/jp/documentation/authenticationservices/))を利用して、認証を行います。

これにより得られた[ASAuthorizationAppleIDCredential](https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidcredential)から`identityToken`(ユーザーのJWT)と`authorizationCode`(サーバー側でトークン処理に必要になる認証コード)を取得し、APIのパラメータとしてサーバー側に送信します。

## 3. サーバー側でリフレッシュトークンを生成する。

アプリから受けとった`identityToken`(JWT)を検証した後、`authorizationCode`を使ってリフレッシュトークンを生成します。

### リクエスト

API: [`POST https://appleid.apple.com/auth/token`](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)

リクエストヘッダ: `content-type: application/x-www-form-urlencoded`

| リクエストパラメータ| 内容 | 取得方法　|
| -- | -- | -- |
| `client_id`| AppID | AppleDeveloperの「Certificates, Identifiers & Profiles」のIdentifiersに載っている`Bundle ID` |
| `client_secret`| AppleDeveloperで生成したのキーを持つJWT | 下記参照 | 
| `code`| 認証コード　| アプリ送信されてきた`authorizationCode` | 
| `grant_type`| 要求のタイプ　| `authorization_code` 指定 |

#### client_secret(JWT)の中身

詳細は[公式](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens)を参考に、下記のclaimをもつJWTを生成します。

ヘッダー

| claim | 内容 | 取得方法 | 
| -- | -- | -- |
| `alg` | JWTのアルゴリズム |　`ES256`指定 | 
| `kid` | AppleDeveloperで作成したキーのID | AppleDeveloperの「Certificates, Identifiers & Profiles」のKeysから作成したKeyのページに載っている`KeyID`|

ペイロード

| claim | 内容 | 取得方法 | 
| -- | -- | -- |
| `iss` | TeamID | https://developer.apple.com/account/#/membership にアクセスした時にURLに表示される末尾の文字 |
| `iat` | JWTの発行日時のUnix時刻。 | 例: python: `int(time.time())`|
| `exp` | JWTの有効期限のUnix時刻。最大15777000 | 例: python: `int(time.time()) + *` |
| `aud` | JWTを受け取るAudience | `https://appleid.apple.com`　指定 |
| `sub` | ServiceID | AppleDeveloperの「Certificates, Identifiers & Profiles」のIdentifersに表示されているBundleID |

### レスポンス

ステータスが200であれば成功です。そのとき下記のようなレスポンスが得られます。

レスポンスの中身から、リフレッシュトークンを取得して保存します。

例

```
{
  "access_token": "adg61...67Or9",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rca7...lABoQ"  # これを保存します
  "id_token": "eyJra...96sZg"
}
```

### 4. アカウント削除時にリフレッシュトークンを使ってトークンを無効化する

[トークン無効化API](https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens)を使って、トークンとユーザーの認証の無効化を行います。

ここで、サインアップ時に取得していたリフレッシュトークンが必要になります。

API: [`POST https://appleid.apple.com/auth/revoke`](https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens)

リクエストヘッダ: `content-type: application/x-www-form-urlencoded`

| リクエストパラメータ| 内容 | 取得方法　|
| -- | -- | -- |
| `client_id`| AppID| AppleDeveloperの「Certificates, Identifiers & Profiles」のKeysから作成したKeyのページに載っている`KeyID` |
| `client_secret`| AppleDeveloperで生成したのキーを持つJWT | 上部参照 | 
| `token`| リフレッシュトークンもしくはアクセストークン　| 今回はサインアップ時に保存していたリフレッシュトークン | 
| `token_type_hint`| トークンのタイプ　| `refresh_token` 指定 |

レスポンスが200であれば成功です。

## 発生したエラー

- `invalid_client`: `client_secret`(JWT)に、間違いがある場合。
   - ここに指定するのは、アプリから送られてきた`identityToken`（ユーザーのJWT）ではなくAppleDeveloplerのキーを使って生成したJWTです。
   - 特に、どのIDがAppleDeveloperのどこから取得できるのか調査するのに時間がかかりました。

- `invalid_grant`: その他のパラメータにエラーがある場合。

## 最後に

私の場合のつまづきどころは、リフレッシュトークンのリクエスト時に必要になるJWTの生成でした。（何回も`invalid_client`と言われて挫けそうになりました。）

そもそも「なぜ必要なのか」「JWTとは何か」ということから勉強し直すことになりました。

ただ、ドキュメントを1つずつ読んで、丁寧に調べていけば、割と簡単に対応することができました。

できてよかった！！！

## 参考URL

- https://developer.apple.com/news/?id=12m75xbj
- https://developer.apple.com/jp/documentation/authenticationservices/
- https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
- https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens