Sign in with Apple のユーザーを別のチームに移行してみる

[CYBIRD Advent Calendar 2024](https://qiita.com/advent-calendar/2024/cybird) 22日目担当の@cy-tsuzawaです。
21日目は@R_araiさんの「AGPの更新でBuildConfig.VERSION_CODEが参照出来なくなったお話」でした。

# はじめに
普段はサーバーサイドエンジニアをしています。
今回、Sign in with Apple を使用しているアプリを別の開発者チームに移行する機会があり、その時に直面した課題やおこなった作業について書いていきたいと思います。

# 概要
Sign in with Apple を使用しているサービスを別の開発者チームに移行する場合、移行後に既存のアカウントにログインできない状況が発生することがあります。
原因として、Sign in with Apple を使用してユーザーが認証をおこなう際に、「メールを非公開」を選択すると`privaterelay.appleid.com`のようなメールアドレスが自動生成されるのですが、生成にチームIDやキーID等のチーム固有の情報が使用されるためとなります。

ちなみに自動生成されたメールアドレスは以下で確認できます。
> iPhoneの設定 > Apple ID > サインインとセキュリティ > Apple でサインイン > 対象のアプリ

Appleにて移行手順について[詳細な記事](https://developer.apple.com/documentation/sign_in_with_apple/transferring_your_apps_and_users_to_another_team)が公開されていますが、結構手順が多く準備に時間がかかるので注意が必要です。

# 準備
記事を確認すると、以下の手順が記載されています。
- ユーザーアクセストークンを取得する
    - client_secret (JWT) の生成
- 転送識別子を生成する
- 移行先のチームに転送識別子を共有

それぞれ1つずつ実行していきます。

# ユーザーアクセストークンを取得する
ユーザーアクセストークンを取得するには、移行元の `client_id` と `client_secret` を取得する必要があります。

`client_id`は、`App ID` または `Services ID`を指定します。Sign in with Apple を使用しているのがアプリなのかWebなのかで指定するIDが変わります。

`client_secret`の取得には、JWTの生成が必要なため次の項目で説明します。

## client_secretを取得する
`client_secret`は [こちら](https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret)に生成手順が記載されています。
生成には、以下の情報が必要です。Apple Developer サイトで確認できます。
- kid：[Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/)にて作成されている Sign In with Apple 用のキーのID
- iss：チームID
- sub：App ID または Services ID

情報が揃ったら、kidの秘密キーで署名されたJWTを作成します。
Pythonで作成してみました。
```
import pathlib
from datetime import datetime, timezone, timedelta
import jwt
    
header = {
    "alg": "ES256",
    "kid": kid
}
payload = {
    "iss": iss,
    "iat": datetime.now(timezone.utc).timestamp(),
    "exp": (
        datetime.now(timezone.utc) + timedelta(days=1)
    ).timestamp(),
    "aud": "https://appleid.apple.com",
    "sub": sub
}
client_secret = jwt.encode(
    payload,
    (pathlib.Path('secrets/AuthKey_XX.p8')).read_text(),
    algorithm="ES256", 
    headers=header
)
```
これで`client_secret`の準備ができました。

## ユーザーアクセストークンをPOSTメソッドで取得する
`client_id` と `client_secret` が取得できたので、ユーザーアクセストークンを取得します。
```
import requests

access_token_response = requests.post( 
    "https://appleid.apple.com/auth/token", 
    data=dict( 
        grant_type="client_credentials", 
        scope="user.migration", 
        client_id=client_id, 
        client_secret=client_secret 
    ),
    headers={"Content-Type": "application/x-www-form-urlencoded"}
) 
access_token = access_token_response.json()
```
以下のようなレスポンスが返ってくれば成功です。
```
{
  "access_token":"XXXXXXXXXXXXX",
  "token_type":"Bearer",
  "expires_in":3600
}
```

# 転送識別子を生成する
ユーザーアクセストークンの取得ができたら、やっと転送識別子の生成です。
今まで使用した情報の他に、以下の情報が必要になります。
- sub：Apple が提供するチームスコープのユーザー識別子。`000000.00000000000000000000000000000000.0000`のような構成
- target：移行先のチームID
```
transfer_sub_response = requests.post( 
    "https://appleid.apple.com/auth/usermigrationinfo", 
    headers={"Authorization": "Bearer " + access_token['access_token']}, 
    data=dict( 
        sub=sub, 
        target=target,
        client_id=client_id,
        client_secret=client_secret
    ) 
)
transfer_sub = transfer_sub_response.json()
```
以下のようなレスポンスが返ってくれば成功です。
```
{
  "transfer_sub":"XXXXXXXXXXXXX"
}
```

# 移行先のチームに転送識別子を共有
ユーザー分の転送識別子の生成が終わったら、転送識別子を移行先に共有します。
移行先は転送識別子を使用して、ユーザーの移行先でのメールアドレスを取得することができます。

# 最後に
Sign in with Apple を使用したサービスを開発する機会は多いと思うのですが、ユーザーの移行の機会はほとんどないので、貴重な経験となりました。
移行後の猶予が60日間となっているので、計画的な準備・移行作業をおすすめします。
ここまで読んでいただきありがとうございました！

明日の[CYBIRD Advent Calendar 2024](https://qiita.com/advent-calendar/2024/cybird) 23日目は@cy-yamakenさんの記事です。お楽しみに！