Migrating Sign in with Apple users for an app transfer

Dear Apple Developer Technical Support,

We are currently following the official Apple documentation “TN3159: Migrating Sign in with Apple users for an app transfer” to carry out a Sign in with Apple user migration after successfully transferring several apps to a new developer account.

Here is a summary of our situation:

  • Under the original Apple developer account, we had five apps using Sign in with Apple, grouped under a shared primary app using App Grouping.

  • Recently, we transferred three of these apps to our new Apple developer account via App Store Connect.

  • After the transfer, these three apps are no longer associated with the original primary App ID. We reconfigured individual Services IDs for each app in the new account and enabled Sign in with Apple for each.

  • More than 24 hours have passed since the app transfer was completed.

Now we are attempting to follow the migration process to restore user access via the user.migration flow. Specifically, we are using the following script to request an Apple access token:


url = "https://appleid.apple.com/auth/token"

headers = {"Content-Type": "application/x-www-form-urlencoded"}

data = {

    "grant_type": "client_credentials",

    "scope": "user.migration",

    "client_id": "com.game.friends.ios.xxxx",  # New Primary ID in the new account

    "client_secret": "<JWT signed with new p8 key>"

}

response = requests.post(url, headers=headers, data=data)

However, the API response consistently returns:

{

"error": "invalid_client"

}

We have verified that the following configurations are correct:

  • The client_secret is generated using the p8 key from the new account, signed with ES256 and correct key_id, team_id, and client_id.

  • The client_id corresponds to the Services ID created in the new account and properly associated with the migrated app.

  • The scope is set to user.migration.

  • The JWT payload contains correct iss, sub, and aud values as per Apple documentation.

  • The app has been fully transferred and reconfigured more than 24 hours ago.

Problem Summary & Request for Support:

According to Apple’s official documentation:

“After an app is transferred, Apple updates the Sign in with Apple configuration in the background. This can take up to 24 hours. During this time, attempts to authenticate users or validate tokens may fail.”

However, we are still consistently receiving invalid_client errors after the 24-hour waiting period. We suspect one of the following issues:

  1. The transferred apps may still be partially associated with the original App Grouping or primary App ID.

  2. Some Sign in with Apple configuration in Apple’s backend may not have been fully updated after the transfer.

  3. Or the Services ID is not yet fully operational for the transferred apps in the new account.

We kindly request your assistance to:

  1. Verify whether the transferred apps have been completely detached from the original App Grouping and primary App ID.

  2. Confirm whether the new Services IDs under the new account are fully functional and eligible for Sign in with Apple with user.migration scope.

  3. Help identify any remaining configuration or migration issues that may cause the invalid_client error.

  4. If necessary, assist in manually ungrouping or clearing any residual App Grouping relationships affecting the new environment.

We have also generated and retained the original transfer_sub identifiers and are fully prepared to complete the sub mapping once the user.migration flow becomes functional.

Thank you very much for your time and support!

import jwt
from datetime import datetime, timedelta, timezone
from cryptography.hazmat.primitives import serialization


# 其他配置示例(注释掉的备用配置)
KEY_ID = "Z7S8S2xxxx"
TEAM_ID = "6VNZUKxxxx"
CLIENT_ID = "com.game.friends.ios.toptop.xxxx"
P8_KEY_FILE = "AuthKey_Z7S8S2xxxx.p8"

def generate_client_secret():
    """使用p8密钥生成JWT格式的client_secret"""
    try:
        # 从文件读取 p8 密钥内容
        # with open(P8_KEY_FILE, 'r') as f:
        #     p8_key_content = f.read()
        p8_key_content = serialization.load_pem_private_key(
             open(P8_KEY_FILE, 'rb').read(),
             password=None
         )
        print(f"从文件读取密钥: {p8_key_content}")
        
        # 计算时间戳
        now = datetime.now(timezone.utc)  # 替代弃用的 utcnow()
        issued_at = int(now.timestamp())
        expiration = int((now + timedelta(days=180)).timestamp())
        
        # 创建JWT头部
        headers = {
            "alg": "ES256",
            "kid": KEY_ID,
        }
        
        # 创建JWT有效载荷
        payload = {
            "iss": TEAM_ID,
            "iat": issued_at,
            "exp": expiration,
            "aud": "https://appleid.apple.com",
            "sub": CLIENT_ID
        }
        print("生成的 payload:", payload)
        
        # 生成签名的JWT作为client_secret
        client_secret = jwt.encode(
            payload,
            p8_key_content,
            algorithm="ES256",
            headers=headers
        )
        
        return client_secret
        
    except FileNotFoundError:
        print(f"密钥文件未找到: {P8_KEY_FILE}")
        return None
    except Exception as e:
        print(f"生成client_secret失败: {str(e)}")
        return None

def get_apple_token():
    """获取Apple的访问令牌"""
    # 生成JWT格式的client_secret
    client_secret = generate_client_secret()
    print("生成的 client_secret:", client_secret)
    if not client_secret:
        print("无法生成client_secret,退出")
        return None
    
    # 设置请求参数
    url = "https://appleid.apple.com/auth/token"
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {
        "grant_type": "client_credentials",
        "scope": "user.migration",
        "client_id": CLIENT_ID,
        "client_secret": client_secret,
        "redirect_uri": "https://example.com/auth/callback"
    }
    
    try:
        # 发送请求
        response = requests.post(url, headers=headers, data=data)
        response.raise_for_status()  # 检查HTTP错误
        
        # 返回JSON响应
        return response.json()
        
    except requests.exceptions.HTTPError as err:
        # 处理HTTP错误
        print(f"HTTP错误 ({err.response.status_code}):")
        if err.response.text:
            print(err.response.text)
        return None
    except Exception as e:
        # 处理其他错误
        print(f"请求失败: {str(e)}")
        return None

# 主程序入口
if __name__ == "__main__":
    # 获取访问令牌
    token_response = get_apple_token()
    if token_response:
        print("\n成功获取Apple访问令牌:")
        print(f"访问令牌: {token_response.get('access_token')}")
        print(f"令牌类型: {token_response.get('token_type')}")
        print(f"有效期: {token_response.get('expires_in')}秒")
    else:
        print("\n无法获取访问令牌,请检查错误信息")
``` This is our code for obtaining the token. Confidential information has been hidden.

Migrating Sign in with Apple users for an app transfer
 
 
Q