こんにちは、開発チームの冨田です。
今日はSign in with Appleにおける、サーバー側でのリフレッシュトークンの取得と、トークンの無効化処理について書きます。
これは2022年6月から適応される新しいガイドラインにて、アカウント削除機能に対する要件が新しく定義され、それに対応するためのものです。
背景
Appleからアカウント削除時の必須要件に関するお知らせがあり、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でのサインインを提供していましたが、トークンの無効化処理を行なっていませんでした。そこで今回、トークンの無効化処理を加えました。
本記事では、トークンの無効化処理に必要なリフレッシュトークンの取得方法と、トークンの無効化処理について説明します。
今までのアカウント処理の流れ(改修前)
今までは下記のフローでログイン系の処理を行なっていました。
全体としては、iOSアプリ側で認証を行い、サーバーでユーザー情報を検証・作成・提供するという流れです。
サインアップ時
アプリ: AppleIDでサインアップする。
アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。
サーバー: アプリから受け取ったJWTを検証し、必要な情報を保存しておく。
サインイン時
アプリ: AppleIDでサインインする。
アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。
サーバー: アプリから受け取ったJWTを検証し、一致するユーザーをアプリに返す。
アカウント削除時
- サーバー: 指定されたアカウントの情報を削除する。
今回改修したアカウント処理の流れ(改修後)
以下の2点を変更しました。
[トークンを無効化するAPI] *1では、リフレッシュトークンもしくはアクセストークンが必要です。一般的にサインアップからアカウント削除までには長時間が開くため、長期的に有効であるリフレッシュトークンを保持するようにしました。
事前準備
サインアップ時
アプリ: AppleIDでサインアップする。
アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。
サーバー: アプリから受け取ったJWTを検証し、必要な情報を保存しておく。
(今回付け加えたところ) サーバー: アプリから受け取った認証コードを元にAPIを元にリフレッシュトークンを取得し、保存しておく。
サインイン時
アプリ: AppleIDでサインインする。
アプリ: 1で得られたJWT(ユーザー情報を持つ)と認証コード(リフレッシュトークンを取得するのに必要)をサーバーに渡す。
サーバー: アプリから受け取ったJWTを検証し、一致するユーザーをアプリに返す。
アカウント削除時
リフレッシュトークンの取得とそれを利用したトークンの無効化処理
ではここから本題の、今回加えたサインアップ時のリフレッシュトークンの取得と、アカウント削除しのトークンの無効化処理の方法について記載します。
1. Apple Developerからキーを作成する
サーバーからアプリのサーバーにAPIリクエストを送る時に必要なキーを生成します。
Developer -> 「Certificates, Identifiers & Profiles」 -> Keysから作成します。
キーのタイプに「Sign in with Apple」を選択し、AppIDを指定します。
キーが生成されるので、ダウンロードして、必要な場所に保存します。
(ピリカではGCPのSecretManagerを利用しており、そこにアップロードしました。)
2. アプリ側の認証時にauthorizationCodeを取得しサーバーに送信する。
アプリでは、AuthenticationServicesフレームワーク(参考)を利用して、認証を行います。
これにより得られたASAuthorizationAppleIDCredentialからidentityToken
(ユーザーのJWT)とauthorizationCode
(サーバー側でトークン処理に必要になる認証コード)を取得し、APIのパラメータとしてサーバー側に送信します。
3. サーバー側でリフレッシュトークンを生成する。
アプリから受けとったidentityToken
(JWT)を検証した後、authorizationCode
を使ってリフレッシュトークンを生成します。
リクエスト
API: POST https://appleid.apple.com/auth/token
リクエストヘッダ: content-type: application/x-www-form-urlencoded
リクエストパラメータ | 内容 | 取得方法 |
---|---|---|
client_id |
AppID | AppleDeveloperの「Certificates, Identifiers & Profiles」のKeysから作成したKeyのページに載っているKeyID |
client_secret |
AppleDeveloperで生成したのキーを持つJWT | 下記参照 |
code |
認証コード | アプリ送信されてきたauthorizationCode |
grant_type |
要求のタイプ | authorization_code 指定 |
client_secret(JWT)の中身
詳細は公式を参考に、下記の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を使って、トークンとユーザーの認証の無効化を行います。
ここで、サインアップ時に取得していたリフレッシュトークンが必要になります。
API: POST https://appleid.apple.com/auth/revoke
リクエストヘッダ: 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つずつ読んで、丁寧に調べていけば、割と簡単に対応することができました。
できてよかった!!!