Dealing with Apple 2FA for iOS automated app delivery
Starting in February 2021, Apple added an additional layer of security by making it mandatory to use the two-factor authentication or two-step authentication system when signing users into the App Store Connect.
Although the two-factor authentication is a great asset for account security, the fact that it was made mandatory has caused some trouble on our CI/CD pipeline. In this article we’ll see how we can overcome those issues.
Using the new API
At WWDC18, Apple announced an API for App Store Connect automation. It provides a familiar REST API designed to facilitate the automation of many tasks that we would normally do manually on the website or using fastlane.
Historically, fastlane has been using Apple ID with username and password to log into the Apple Developer Portal and App Store Connect so it can update the certificates, mobile provisioning profiles, upload app binaries, etc. This method worked quite well throughout the years. But since fastlane shipped a new version that leverages the new Apple API, it is recommended to use the latter when we are able to.
Fastlane will continue to use these two ways of authentication since there are benefits and but still some downsides when using the new API.
Pros:
- No 2FA needed
- Better performance
- Documented API
- Reliable, stable, and recommended by Apple
Cons:
- Cannot be used with Enterprise accounts
- There is no way to download dSYM files
If you are not affected by these downsides, migrating to App Store Connect API is a way to solve the 2FA problem.
To do this, the Apple account holder needs to generate the key from appstore connect and add it the CI as an environnement variable, for exemple: APP_STORE_CONNECT_API_KEY_CONTENT
.
The app_store_connect_api_key
action sets the API key value into the global lane context dictionary and actions like pilot
will automatically load the API Key from the lane_context
dictionary.
Using Apple ID:
For Enterprise accounts, using Apple ID with fastlane is the only way to go.
Fastlane has recently added an option to opt-in to skipping 2FA upgrade with the SPACESHIP_SKIP_2FA_UPGRADE=1
environnement variable. This feature is available on version 2.173.0 and works only for accounts that still has not enabled the 2FA. It’s a short term solution before 2FA is fully forced by Apple, and can be used to buy time while implementing a more sustainable solution.
At Fabernovel, we have a main Apple account that manages certificates on multiple teams. It is used by our CI/CD system and project managers / developers to access the developer Portal and App Store connect.
With 2FA enabled, we have to ensure that this account is accessible by both parties without much trouble logging-in.
The idea is to use fastlane’s spaceauth command line to generate a FASTLANE_SESSION
(cookie) and add it as an environment variable of our CI system. This session will be reused instead of triggering a new login each time fastlane communicates with Apple’s APIs.
Under the hood, spaceauth is really just making HTTP requests to App Store connect with the provided username and password. It needs the 2FA authentication code to be able to log in and retrieve the cookie.
We needed a way to automate the retrieval of the 2FA code and post it to our Slack so that people can use it whenever they try to log-in. And also when using spaceauth to generate the fastlane session each month (or so we thought). So we tried different leads.
2FA code retrieval with Twilio:
Using SMS:
Our first plan was to use a Twilio number as trusted phone on our Apple account, then retrieve the SMS containing the 2FA code using a webhook, and send it to slack. Sounds easy right ?
Unfortunately, we were never able to receive any 2FA code using a Twilio number. After a little digging it turns out the problem was that Apple uses “short code numbers” to send 2FA codes, and receiving SMS from these kind of numbers is not a feature that is enabled by default on Twilio.
After getting in touch with the support to enable communication with short numbers, we found out that this feature is not fully supported and has multiple restrictions. Short Numbers can only be used on to receive SMS from the same country, and even then, Twilio does not guarantee that all SMSs will be received. Using their numbers for 2FA codes is not something that they recommend :
These codes are supposed to be sent to mobile devices. Apple may intentionally not route them to Twilio, or it may suddenly stop working one day, so it is not recommended to use Twilio numbers for this use.
Using a phone call
Another quick attempt that we tried was to use a phone call instead of SMS as we noticed that Apple does not use short numbers in this case. We used the “record and transcribe” feature provided by Twilio but with no luck. Apple hangs out as soon as we begin to record the call.
By then, it was clear that using Twilio wasn’t really convenient for our use case.
Using our own CI machine.
Besides using a trusted phone number to receive the 2FA code, we can also use a trusted macOS device that displays the 2FA code when we try to log in.
We had this crazy idea of retrieving the code using the automation scripting language Applescript that worked quite well surprisingly.
This script runs on one of our CI machines every minute to check if there is any 2FA UI open, then retrieve the text code and send it to slack directly so that anyone trying to connect may receive the code on a dedicated channel.
We later realized, after some tests, that the FASTLANE_SESSION
provided by spaceauth only lasts for eight hours. So manually interacting with spaceauth and updating the environment variable on our CI wasn’t an option anymore, we needed a way to automate this process.
Spaceauth can only be used with interactive-mode. Therefore, we created a ruby script that is scheduled multiple times a day to interacts with it using the ruby_expect
library so we can get and update the session.
After the session retrieval we just had to update the new value as an organization secret using the Github API and voila.
With this setup, we were able to connect manually to our main Apple account using the first script that checks periodically for the pop-up and sends the 2FA code to slack. And we had our FASTLANE_SESSION
updated many times a day so that our CI uses a valid session token.
Conclusion
Implementing a solution to properly retrieve and update our CI with a fastlane session environment variable wasn’t really a trivial task as it seemed at first. We ended up using custom scripts on one of our CI machines that is trusted by our main Apple account to retireve the code to be sent to slack or used in spaceauth. This is very specific to our organization, you may have other constraints, but the same ideas can be reused.