Update (2020)

Thanks to the great team behind Fastlane, the newly added support for the App Store Connect API resolves most of the issues one could face with enforced 2FA.

Here is what you need to do to migrate from username / password to an API key:

  • Create a new App Store Connect API Key in the Users page
  • Download the newly created API Key file (.p8)
  • Update the lanes you are using:
lane :release do
api_key = app_store_connect_api_key(
key_id: "YOUR_KEY",
issuer_id: "YOUR_ISSUER_ID",
key_filepath: "./<YOUR_PATH.p8",
in_house: false, # Set it to `true` for Enterprise accounts
)

pilot(api_key: api_key)
end

That's it! No need to mess with SMS parsers anymore.

Bear in mind that passing hardcoded values is a huge security risk, so better to store them as environment values.

I will keep the approaches below as a reminder of struggles we as a community went through.


Apple recently started to enforce 2FA for account holders and preventing uploading builds without 2FA:

In an effort to keep your account more secure, two-factor authentication will be required to sign in to your Apple Developer account and Certificates, Identifiers & Profiles starting February 27, 2019. This extra layer of security for your Apple ID helps ensure that you're the only person who can access your account. If you haven't already enabled two-factor authentication for your Apple ID, please learn more and update your security settings. [1]

And while that's a great step to ensure developers are a bit more protected, managing SMS codes is pretty hard when it comes to CI pipelines, so I've decided to share a few approaches I tried to overcome this inconvenience.

🤓 A bit of context

So, what is 2FA in the first place?

2FA stands for 2-factor authentication, which is a type of multi-factor authentication, and essentially is a method of confirming a user's claimed identity by providing multiple pieces of evidence to an authentication mechanism.

In real world you might experience it when providing a driving license and a passport, but in the Internet usually websites allow you to choose the second piece of evidence from a limited variety of options: SMS codes, codes generated by a dedicated app, or backup codes from a pre-generated list.

The best practice in that case is usually to store everything in a password manager of your choice, as the majority of them can generate codes as well.

🚀 What's wrong?

However, if you use Fastlane or any other tool to interact with App Store Connect, a month ago your workflows probably broke, as Apple made 2FA mandatory for account holders.

It leads to multiple solutions with their own pros and cons:

  1. We can use a console 2FA generator.
  2. We can SSH into a CI machine and provide the 2FA manually.
  3. We can generate an app-specific password and use it instead of 2FA codes.
  4. We can generate a temporary session and provide it via an environment variable every time we prepare a build.
  5. We can use Twilio or any other communication API to parse SMS codes for 2FA and pass to CI.
  6. We can create a user without 2FA and restricted admin privileges.

Even though some of those counterfeit the whole concept of multi-factor authentication, in such a desperate case it's worth considering. Unfortunately, it's easy to tell that options 1 and 2 won't really work out:

  1. Apple’s 2FA is different from normal 2FAs [2], so you can’t use 1Password or any console tool as a code generator. You should either use an SMS, or any macOS/iOS device where you signed in with the same Apple ID. Which means it won't be possible (unless you somehow get Corellium or any other security utility to simulate iOS).
  2. While Fastlane works perfectly with 2FA and will ask you for a verification code when needed, the whole purpose of remote CI servers is to remove a human from the equation. If we want to provide codes manually, someone with a verification device should be ready to ssh into a machine in the middle of the night (and the majority of CI setups have a timeout).

Let's discuss the rest in more details.

🔑 An app-specific password

App-specific passwords are single-use passwords for your Apple ID that let you sign in to your account and securely access the information you store in iCloud from a third-party app. [3]

They're fairly easy to generate if you already have 2FA enabled:

  • Sign in to your Apple ID account page.
  • In the Security section, click Generate Password below App-Specific Passwords.
  • Follow the steps on your screen.

Once you have a password, set it as a FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD environment variable, and that will unlock iTMSTransporter so your CI will be able to upload new builds.

What's the caveat? The application specific password will not work if you need anything else except uploading the binary, e.g. fetching and increasing build number, updating screenshots, or distributing to testers.

⏳ Temporary sessions

Another way to resolve this issue is to use fastlane spaceauth -u user@email.com to generate a temporary session and store it in FASTLANE_SESSION environment variable. That's a pretty good solution, as it allows you to do everything you want with your account given the session is still valid.

However, in real world sessions live around 6-8 hours, and have to be renewed manually after that. Also, using multiple containers in parallel and/or in different locations might cause the session to expire even earlier, so it's pretty inconvenient.

Any way to improve that flow? Probably ssh into your CI machine and teach it to request fresh sessions again and again, and update environment variables automatically. Unfortunately that requires too much time to implement and support.

💬 SMS parsing

We can use something like Twilio to parse SMS codes for 2FA, and provide them on CircleCI. If you decide to go that lane, take into account that Twilio and other similar services are usually paid, though the setup itself is pretty fast: once you have an account, a phone number, and configured environment, just create a webhook to launch your custom script on any received message:

Now stuff gets a bit tricky: we have to launch a listener to wait for a 2FA and once it's parsed print it out or set as an env variable. At the same time, we want fastlane receive this output too. To solve that, we can disable the internal buffer and also launch a Flask server to wait for the code.

from flask import Flask, request

app = Flask(__name__)

@app.route("/sms", methods=['GET', 'POST'])
def sms_parsing():
os.environ["2FA_CODE"] = code_from_sms
print code_from_sms


if __name__ == "__main__":
app.run(debug=True)
import os
import sys
from subprocess import Popen, PIPE, STDOUT
script_path = os.path.join(get_script_dir(), 'sms_server.py')
p = Popen([sys.executable, '-u', script_path],
stdout=PIPE, stderr=STDOUT, bufsize=1)
with p.stdout:
for line in iter(p.stdout.readline, b''):
print line,
p.wait()

Unfortunately, this approach is not secure and requires both additional setup and a third-party service, but at the same time solves the problem.

🔓 A no-2FA account

We can create a user with restricted admin privileges and without 2FA, which is still possible as it is not an account holder, and use it the same way we were previously, but one day Apple might force everyone to use 2FA and that will break again.

My initial idea was just to take a sandbox user I already had, and downgrade its account to No-2FA. Apparently, once you setup 2FA, it's impossible to disable. Sure makes sense, so I end up creating a separate account, and provided its credentials to FASTLANE_USER and FASTLANE_PASSWORD environment variables on your CI machine. If you use Appfile, you can set it there as well.

This allows you to access both uploading builds and interacting with App Store Connect account, which is very good. However, that's not secure enough, and given Apple already forced account holders to use 2FA, there is a huge chance everyone else will be forced to do the same in the nearby future, so the flow will break again.

🎬 Summary

We've had a brief look at multi-factor authentication concept, as well as pros and cons of various approaches to make it work with Fastlane.

It's up to you which one to choose, and I probably will stick to a no-2FA user for now, however temporary sessions after some polishing might be a better and way more stable solution.

If you want more information on fastlane setup for CI, check out their documentation, or have a look at some of the most recent discussions regarding enabled 2FA [4][5][6].


  1. Upcoming Two-Factor Authentication Requirement for Account Holders ↩︎

  2. Speaking about Apple's 2FA, it's worth mentioning 2SV as well, because 2FA (2 factor authentication) and 2SV (2 step verification) are two different things at Apple. 2SV is the old system, where you get a 4 digit code to verify that worked on devices older than iOS 11. 2FA is the newer system, you get a 6 digit code, and only works for devices from iOS 11. Luckily, Fastlane supports both. ↩︎

  3. Using app-specific passwords ↩︎

  4. Does Fastlane support Apple's mandatory two factor authentication? ↩︎

  5. Observations on the Fastlane/Spaceship session length ↩︎

  6. Automatically update FASTLANE_SESSION Environment Variable ↩︎