Apple has enforced two-factor authentication for all App Store Connect users. This caught me off guard and caused my iOS app release pipeline to fail.

You can read more about the enforcement of 2FA in here, in which they say

Starting February 2021, two-factor authentication or two-step verification will be required for all users to sign in to App Store Connect. This extra layer of security for your Apple ID helps ensure that you’re the only person who can access your account.

Why?

Background

My iOS release pipeline is using Apple App Store task for Azure DevOps, which under the hood uses fastlane - a tool that communicates with App Store Connect in order to (in my case at least) upload a new build to TestFlight.

The connection that fastlane establishes with App Store Connect is done via a "service" account user that I've set up for myself. I call it service account but it's a regular Apple Id account that has enough permissions to the app(s) that I'm publishing. This then allows fastlane to use that account to authenticate with App Store Connect and push new versions of the app to TestFlight.

The Incident

Few days ago I tried to release an update to one of my apps and iOS release pipeline have failed with the following message from the fastlane pilot command:

[!] The request could not be completed because: Need to acknowledge to Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com (or https://appstoreconnect.apple.com) to acknowledge the statement. Your account might also be asked to upgrade to 2FA. Set SPACESHIP_SKIP_2FA_UPGRADE=1 for fastlane to automaticaly bypass 2FA upgrade if possible.

That brought me almost straightaway to this github issue, which made me realise almost instantly that the "service" account that I've set up for myself is not using 2FA. That was intentional though. The reason behind keeping the account less secure (i.e. without 2FA) was dictated by the fact that if you turn the 2FA on, your CD pipeline would die every month because the two-factor authentication session for your "service" account would expire and you would need to recreate it manually. I'm pretty lazy, therefore I definitely didn't like the idea of dealing with "fixing" my pipeline once a month. As you can imagine, I was not very pleased about the fact that Apple has decided to make the 2FA compulsory for all of the accounts and now I'm facing a monthly Sisyphean task.

Research

Fortunately, there is such thing as App Store Connect API, which I think is still fairly "new" (definitely to me) but a recommended way of working with App Store Connect. Unfortunately, even though it has been announced as part of the WWDC18 it is still lacking some features and therefore it might not be suitable for your purposes.

App Store Connect API - Apple Developer
This API lets you automate tasks on App Store Connect for increased efficiency. Use it for development, testing, and reporting within your team’s internal workflow.

I was very pleased to hear that there's a way of avoiding using Apple Id account. However, I got pretty sad when I learned, that the Apple App Store task for Azure DevOps was not supporting the App Store Connect API, at least not yet...or was it?

As of Feb 16th the PR made by Johannes Fahrenkrug with changes to support the App Store Connect API have been merged in and new release (version 1.183.0) of the task has been pushed out.

At the moment there's no documentation on how to upgrade your existing DevOps task from Apple Id way of communicating with App Store Connect to using the Api Key, so I will try to walk you through it.

Solution

You can read about the App Store Connect API over here. On the bottom of that page you can find a link to the App Store Connect Api panel, where you can request access (aka create your API key) - in case you haven't already. Important detail to note is that you need to be an:

Account Holder of your Apple Developer Program membership.

Follow that link and go to the Keys tab

App Store Connect Api panel

Note: This screen will look slightly differently for whoever is generating their first API key.

If you do or don't have an API key, I would suggest creating a new one that will be devoted purely for your CD pipeline purposes.

When generating a key you probably want to name it something along the lines of DevOps or CD Pipeline, so it's purpose is clear. From the Access drop down select App Manager role (you might get away with Developer role as well, although I haven't verified if it has sufficient permissions to use pilot command).

Generating the App Store Connect API key

After hitting the Generate button your newly created API key should appear on the list

App Store Connect API key

At this stage we need to take a note of few things on this screen, as we will use this information when updating the Apple App Store task in your Azure DevOps pipeline. We will need:

  • Issuer Id
  • Key Id
  • Download the API Key - *.p8 file

Note: The Download API Key hyperlink on the right side of the row will disappear after you download the file - there won't be another time to download it, so make sure you store it in a secure place.

Let's update our task in the pipeline. Before the changes to use the API key my App Store Release task looked as follows:

- task: AppStoreRelease@1
  displayName: 'Publish to TestFlight'
  inputs:
    serviceEndpoint: 'Apple App Store'
    appIdentifier: '$(AppPackageName)'
    appType: 'iOS'
    releaseTrack: 'TestFlight'
    ipaPath: '$(Pipeline.Workspace)/*.ipa'
    shouldSkipWaitingForProcessing: true
    shouldSkipSubmission: true

After I incorporated API key changes the task looked like this

 - task: AppStoreRelease@1
   displayName: 'Publish to TestFlight'
   inputs:
     authType: 'ApiKey'
     apiKeyId: '<Key Id>'
     apiKeyIssuerId: '<Issuer Id>'
     apitoken: '$(AppStoreConnectApiKeyContentBase64)'
     appIdentifier: '$(AppPackageName)'
     appType: 'iOS'
     releaseTrack: 'TestFlight'
     ipaPath: '$(Pipeline.Workspace)/*.ipa'
     shouldSkipWaitingForProcessing: true
     shouldSkipSubmission: true

There's few things to note here:

  1. The version of the task has not changed, it's still v1
  2. The serviceEndpoint is gone
  3. New properties have appeared authType, apiKeyId, apiKeyIssuerId and apitoken.

Hopefully besides apitoken everything is clear and pretty self explanatory. The apitoken though is a base64 generated string from the content of your *.p8 API key file. Your *.p8 file content should look like this:

-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgYLcY49N43tA1oLy5
QcEeOqoLh1iJ4RGGu0oCfOqkK+CgCgYIKoZIzj0DAQehRANCAAS0rlFZAoiSgFp2
KSWS+VXl6LPn3o+sbegfiv8lADv1+u7nUMOpyl+aSrH0kPyRWnmuEWiEuwL0owhT
gnpVnxmr
-----END PRIVATE KEY-----
App Store Connect Api Key content

What I've done was to use the following PowerShell script command to generate base64 string out of the API key content:

$api_key_content = '<your_p8_file_content_here>'
[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($api_key_content))

The result of the above will look something like this

LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR1RBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJIa3dkd0lCQVFRZ1lMY1k0OU40M3RBMW9MeTUKUWNFZU9xb0xoMWlKNFJHR3Uwb0NmT3FrSytDZ0NnWUlLb1pJemowREFRZWhSQU5DQUFTMHJsRlpBb2lTZ0ZwMgpLU1dTK1ZYbDZMUG4zbytzYmVnZml2OGxBRHYxK3U3blVNT3B5bCthU3JIMGtQeVJXbm11RVdpRXV3TDBvd2hUCmducFZueG1yCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=

Instead of doing this on your machine you could upload the *.p8 file to the Library, download it with the Download Secure File task, use a bash script (or any other script language of your choice) to read the file and generate bas64 string based on that.

If you do that outside of your pipeline, as I've done, you need to take the generated base64 string and ideally store it in a secure variable. Reference the generated string in the Apple App Store task - see the $(AppStoreConnectApiKeyContentBase64) variable in the snipped above - and you're done.

No more two-factor authentication!