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

こんにちは、開発チームの冨田です。

今日は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アプリ側で認証を行い、サーバーでユーザー情報を検証・作成・提供するという流れです。

サインアップ時

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

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

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

サインイン時

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

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

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

アカウント削除時

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

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

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

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

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

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

事前準備

  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フレームワーク(参考)を利用して、認証を行います。

これにより得られた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つずつ読んで、丁寧に調べていけば、割と簡単に対応することができました。

できてよかった!!!

参考URL