A Great Migration: Transfer your iOS App without Losing Firebase Users
Intro
So you live happily, developing your iOS app. The app uses Firebase Authentication for user management, that handles all the dirty work with OAuth. Your users sign in using Google, Facebook and, of course, Apple Sign-In.
And then the day X comes and you face the need to transfer the app to another Apple developer account.
At first glance, there is nothing special: you just register a new app, associated with a different team, release it as usual. A QA engineer rolls up their sleeves and immediately runs into a bug: after signing in to an existing account using Apple Sign-In, the personal data hasn’t been loaded!
But what happened?
The thing is, when a user chooses to get authenticated via Apple Sign-In, they’re being assigned an automatically generated email address ending with privaterelay.appleid.com
. This only happens when a user opted for hiding the real email. You can read more about private relay emails here, and I’m just going to point out to one specific property:
They’re the same for a user across all apps written by a single development team, and different for the same user across apps written by different development teams.
Ahh, so the development team changed, right?
Yep. And the email changed accordingly, that’s why Firebase created another record in the Auth table, so it’s an entirely different user account now. How do we fix that?
Strayed in a maze of identifiers
There’s actually a detailed article from Apple itself on how to transfer users to another team. The pretty straightforward manual, nevertheless it took a fair bit of time to really make things work.
Let’s take a look from above at what the proposed actions are:
- Obtain the user access token from the old team app.
- Generate the intermediary transfer identifiers, so the new team could use them to import the users.
As the article suggests, in order to retreive the user access token, you have to obtain the old app’s client_id and client_secret:
POST /auth/token HTTP/1.1
Host: appleid.apple.com
Content-Type: application/x-www-form-urlencodedgrant_type=client_credentials&scope=user.migration&client_id={client_id}&client_secret={client_secret}
You can find the client_id
value at Firebase → Project Settings → Your Apps →Bundle ID.
The next step is to generate the client_secret
for the old team. This value is basically a JWT you encode using the structure described here. For that purpose I created a simple Python script:
import pathlib
from dataclasses import dataclass
from datetime import datetime, timedeltaimport jwt
@dataclass
class Client:
id: str
team_id: str
private_key: str
kid: strdef generate_secret(self) -> str:
header = {
"alg": "ES256",
"kid": self.kid
}
payload = {
"iss": self.team_id,
"iat": datetime.utcnow().timestamp(),
"exp": (
datetime.utcnow() + timedelta(days=1)
).timestamp(),
"aud": "<https://appleid.apple.com>",
"sub": self.id
}
return jwt.encode(
payload,
self.private_key,
algorithm="ES256",
headers=header
)
client = Client(
id='com.yourproject.app',
team_id='XYZ345ABC0',
private_key=(
pathlib.Path('/secrets/AuthKey_S1FOOBAR23.p8')
).read_text(),
kid='S1FOOBAR23'
)
client_id = client.id
client_secret = client.generate_secret()
There are lots of different identifiers, that is why this part is called “a maze of identifiers”: to get this value, you should first utilize that key, which is stored there, and so on. But now it’s time to figure them out:
id
is theclient_id
team_id
is located at Firebase → Project Settings → Your Apps →Team ID.private_key
- an Auth key, created via the Apple Developer Account panel. Here is how to get it:
- Go to Certificates, Identifiers & Profiles → Keys
- Click Create a key
- Name your new key and check the Sign In with Apple box
- Click Continue
- Click Downloadkid
stands for Key ID. It is specific for this .p8 auth key, you can find it in the key file’s name
Awesome, at this point we’ve got our client_id and client_secret, so nothing stands in our way of obtaining a user access token:
import requestsaccess_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
)
)
access_token = access_token_response.json()['access_token']
With the access token at hand, we can request a bunch of transfer identifiers with ease:
transfer_sub_response = requests.post(
'<https://appleid.apple.com/auth/usermigrationinfo>',
headers={'Authorization': 'Bearer ' + old_client__access_token},
data=dict(
sub=sub,
target=new_client__team_id,
client_id=old_client__id,
client_secret=old_client__secret
)
)
I believe that the variables starting with old_client__*
and new_client__*
do not require additional comments. Most importantly, the prefixes indicate from which developer team these values should be obtained.
As for sub
, this is “a team-scoped user identifier that Apple provides”, according to the Apple Dev docs. While it’s likely possible to extract sub
via Apple API, I’m bringing Firebase into the picture again. Let’s recall that the aim of our mission is to restore access to the user accounts. To do so, create a table in any nearby database:
Fill the old_relay_email
column (I’m assuming you know how to get the list of anonymous user emails for your app from Firebase, as well as UIDs). Next utilize Firebase SDK for your preferred language and get something called provider_data
for each of the listed users. It’s a JSON array containing provider-specific data, and apple.com defines rawId
that represents, as you may guess, the value for sub
. Don’t forget to populate the table with the corresponding sub
values.
Handing over the precious tokens
When we were transferring our app from one developer team to another, we had full access to both, so the title is mostly just for gigs. Pay attention, however, that all the fuss with creating transfer tokens implies that you must not share the private email addresses specific to your app with the recipient team in case that’s a different company. Keep an eye on your users’ privacy.
That being said, let’s complete the table above with the new_relay_email values. We’re moving on to the next article about exchanging transfer tokens for the new private emails. It follows the same scenario as the previous one:
- get the client_id and client_secret for the new app
- use these credentials to obtain the email addresses
The JSON response you’ll get from appleid.apple.com/auth/usermigrationinfo
should contain a sacred email
field. Enrich the database table with the freshly collected new_relay_email
values. Your table should look like this:
Putting it all together
The only thing left to do is to update the anonymous user email in the Firebase storage. Hence, blow the dust off the beforementioned Firebase SDK, we’ll need that once more.
from firebase_admin import auth as firebase_auth
...
firebase_auth.update_user(user_firebase_uid, email=new_relay_email)
A piece of cake, as you can see. I’ll leave all the hard work with reading the table and iterating over users to you 😉
Conclusion
There is nothing supernatural about transferring anonymous users from an iOS app to another team, and yet, being combined with Firebase Auth, this problem made us dig the docs and tutorials for a decent amount of time. I hope that with this article the process has become faster and more clear for you. Thank you for reading 😃