A Great Migration: Transfer your iOS App without Losing Firebase Users

Julia Este
5 min readJun 25, 2022

--

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:

  1. Obtain the user access token from the old team app.
  2. 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-urlencoded
grant_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, timedelta
import jwt
@dataclass
class Client:
id: str
team_id: str
private_key: str
kid: str
def 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 the client_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 Download
  • kid 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 😃

--

--

Julia Este

I’m a software engineer most experienced in developing backend in Python. I’m also familiar with frontend development and ETL process design.