Migrating Sign in with Apple users to another team
Overview
This walks through migrating Sign in with Apple users to a different team.
When an app that includes Sign in with Apple moves to another team, the user records have to move with it.
Moving an ios app itself to a different account is covered in an earlier post1.
Moving the sign-in records to another team
Generating client_secret
Generate a client_secret for both the current team and the receiving team.
require 'jwt'
key_file = 'key.p8'
team_id = 'TeamID'
client_id = 'AppID'
key_id = 'KeyID'
ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file
headers = {
'kid' => key_id
}
claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}
token = JWT.encode claims, ecdsa_key, 'ES256', headers
puts token
Getting an access_token
Use the current team’s client_id (the app’s bundle id) and client_secret to obtain an access_token. Send grant_type and scope exactly as shown below, unmodified.
client_id: theclient_idused by the current teamclient_secret: the current team’sclient_secret
Request
POST /auth/token HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=user.migration&client_id={client_id}&client_secret={client_secret}
Response
Keep the returned access_token somewhere safe.
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"adg6105ed7a4de...0.nx.20LreF67Or9",
"token_type":"Bearer",
"expires_in":3600
}
Creating a transfer identifier
Now use that access_token to create a transfer_sub. Note that the token goes in the header.
sub: the existing user’sProvider Idrecipient_team_id: the id of the receiving teamclient_id: theclient_idused by the current teamclient_secret: the current team’sclient_secret
Request
POST /auth/usermigrationinfo HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {access_token}
sub={sub}&target={recipient_team_id}&client_id={client_id}&client_secret={client_secret}
Response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"transfer_sub":"760417.ebbf1c...ba852d492d8a.1827"
}
Creating the exchange identifier
Use the transfer identifier transfer_sub to create the exchange identifier.
The same access_token from above continues to be used.
client_id: the new team’sclient_idclient_secret: the new team’sclient_secret
Request
POST /auth/usermigrationinfo HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {access_token}
transfer_sub={transfer_sub}&client_id={client_id}&client_secret={client_secret}
Response
Update the provider Id in your database with the returned exchange identifier sub and the migration is complete.
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"sub":"820417.faa325...2d492d8a.0219",
"email": "ep9...nph@privaterelay.appleid.com",
"is_private_email" : true
}
Note that this step only succeeds for an app that was actually transferred from its owner. Otherwise the response comes back as invalid_client. The provider Id token stays valid for at most 60 days from the ownership transfer date, so this has to be done within that window2.
{
"error": "invalid_client"
}
Conclusion
When an app goes through the normal transfer procedure, the method above does move the provider Id to the new team.
What I ran into was an unusual case. A misconfiguration meant the app belonged to team A while the key file sat with team B, and in that situation the method above cannot complete the migration.
For an app built purely natively, a mismatched client_id registration is not possible. But when a native app is mixed with web views and the Apple ID integration lives in the web view, a mistake can result in a client_id belonging to a different team than the app. That is the case where team A holds only the key file and team B holds only the app. Two possible routes out:
- Ship an app under the same
client_idfrom the current team all the way to release, then transfer it to the new team - Prompt users through the web view to sign in again under the new team, and on success replace the stored
provider Idfrom the old team (this needs a user migration flow built on the web side)