24/7 twenty-four seven

iOS/OS X application programing topics.

Apple IDの2ファクタ認証をCI環境で突破する

【注意】この記事で紹介しているSMS APIサービスのVonageは利用規約により認証にVonageの電話番号を利用することを禁止しているという記述があるので、末尾の別解として載せたAndroidデバイスを使ってSMSを転送する方法が良さそうです。

help.nexmo.com

   

2021年2月から、App Store Connectにログインする際にすべてのApple IDで2ファクタ認証が必須になります。

Starting February 2021, additional authentication 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. You can enable two-step verification or two-factor authentication now for the Apple ID associated with your developer account. Visit the Security section of your Apple ID account or the Apple ID section of Settings on your device.

この変更により、CIでFastlane等を使用してApp Store Connectの操作(バイナリのアップロード、TestFlightや審査の提出、etc.)を自動化しているところに支障をきたします。

これまでは2ファクタ認証が必須だったのはAccount Holderなど強い権限を持つ一部のアカウントだけでしたので、Account Holderではないアカウントを別に作成して、CIではそのアカウントを使用することでこの問題を回避できていました。

あるいはFastlaneは少し前にApp Store Connect APIの利用に完全対応したので、App Store Connect APIのキーを設定して利用する方法もあります。

前者の2FAを無効にしたアカウントを使う方法は2月以降は使えなくなります。また、すでに新規に作成するApple IDは2FAが必須になっており無効にできなくなっています。

理想の解決方法はApp Store Connect APIを利用することですが、App Store Connect APIキーの発行にはAccount Holderの権限が必要なため、クライアントワークの場合などすぐに対応できないこともあると思います。

この記事では2ファクタ認証が有効なApple IDでもCI環境でFastlaneが利用できるようにする方法を紹介します。

SMS送受信API

要するにSMSを受信できるサービスを利用します。

特別なことはないオーソドックスなアプローチです。

ただ、意外とAppleの2FAのSMSが届くサービスがなかったのでこれならいけた、というノウハウの共有になります。

結論をいうとVonageのSMS APIで、オーストラリアの電話番号を使うとAppleのSMSが受信できました。

Vonageにサインアップして「Buy Numbers」から電話番号を取得します。

f:id:KishikawaKatsumi:20210126163252p:plain:w600

Apple IDの2ファクタ認証に使用する電話番号は複数設定できるので、ここで取得した電話番号を追加します。

f:id:KishikawaKatsumi:20210126165346p:plain:w600

余談ですが、Vonageではオーストラリア以外の国の電話番号も取得できます。

いくつかの国の番号を試して、たまたま受信できたのがオーストラリアの番号だったというだけなので他の国の番号でもよいかもしれません。

国によって番号の維持費が違うのでより安価な番号が使えるならそれがよいでしょう。

受信したSMSはReports APIまたはWebのGUIから取得できます。

Incoming Webhookもあるので受信したらどこかに転送する、というのでもいいかもしれません。

Fastlaneのセッションを定期的に更新する

2ファクタ認証が有効なApple IDでFastlaneを利用する場合、App Store Connectのログインが必要なタイミングで2ファクタ認証の番号の入力が求められます。

インタラクティブな環境では番号の入力ができますが、CI環境では番号の入力ができずそこで止まってしまうのでその問題の解決としてログイン成功時のセッションクッキーを事前に生成しておき、それを再利用する方法があります。

具体的にはfastlane spaceauthコマンドにてログインセッションを生成し、セッション変数、あるいは保存されたセッションクッキーのファイルをキャッシュ等に保存して再利用する、となります。

詳しくは下記の公式ドキュメントをご覧ください。

docs.fastlane.tools

spaceauthコマンドによって生成されたセッション情報は、最初の1回は2FAの認証が必要ですが、有効な間に定期的に更新できれば2回目以降は2FAの認証は不要です。

ただしCI環境のマシン構成やタイムゾーンなどが大きく変わった場合には別のマシンと認識されるため再度2FAの認証が求められます。

そこで必要になるのが先ほどのSMS APIによる2ファクタ認証の自動化です。

最初の1回と、なんらかの理由により再度2ファクタ認証が必要になったときにCIの処理が止まってしまわないように自動で2ファクタ認証の番号が入力できるようにしておきます。

下記のコードは実際にヤプリのCIで利用しているFastlaneのセッションを定期的に更新する処理のコードです。

この処理を1日1回CIで定期実行しています。

生成されたセッションクッキーは~/.fastlaneディレクトリに保存されるので、~/.fastlaneディレクトリをCIのキャッシュに保存して、CIの各Jobで復元します。

今のところ、これでCIのJobが2FAにより失敗することはなくなりました。

もしかしたら1日の間にCIマシンの割り当てが大きく変わる、などがあると困るのかもしれませんが今のところ起こっていません。

Vonage APIで確認すると数日に1回は2FAのSMSが届いているようなので、なんらかの要因によりセッションが無効と判断されることがそこそこあるようです。 (複数のCIとBotの環境で利用しているので一般の環境よりは複雑です。)

require "net/http"
require "uri"
require "pty"
require "expect"
require "fastlane"
require "spaceship"
require_relative "./setup_credentials"

def fastlane_spaceauth(user, password, default_phone_number)
  ENV["FASTLANE_USER"] = user
  ENV["FASTLANE_PASSWORD"] = password

  # SPACESHIP_2FA_SMS_DEFAULT_PHONE_NUMBER環境変数を設定すると、
  # 複数の電話番号を登録している場合にどの番号にSMSを送るかを指定できる
  ENV["SPACESHIP_2FA_SMS_DEFAULT_PHONE_NUMBER"] = default_phone_number

  $expect_verbose = true

  # 2FAの入力に応答するためにPTYを使って`fastlane spaceauth`コマンドを実行する
  cmd = "fastlane spaceauth"
  PTY.spawn(cmd) do |i, o|
    o.sync = true

    # 有効なセッションクッキーがある場合は何もしない
    i.expect(/Pass the following via the FASTLANE_SESSION environment variable:/, 10) do |match|
      if match
        o.puts "y"
        return
      end
    end

    # セッションが無効で2FAの入力が必要な場合
    i.expect(/Please enter the 6 digit code you received at .+:/, 60) do |match|
      raise "UnknownError" unless match
      sleep 10

      now = Time.now.utc - 120
      date_start = now.strftime("%Y-%m-%dT%H:%M:%SZ")
      date_end = (now + 120 + 120).strftime("%Y-%m-%dT%H:%M:%SZ")

      api_key = ENV["VONAGE_API_KEY"]
      api_secret = ENV["VONAGE_API_SECRET"]

      # SMS APIから2FAのSMSを取得する
      uri = URI.parse("https://api.nexmo.com/v2/reports/records?account_id=#{api_key}&product=SMS&direction=inbound&include_message=true&date_start=#{date_start}&date_end=#{date_end}")
      request = Net::HTTP::Get.new(uri)
      request.basic_auth(api_key, api_secret)

      options = {
        use_ssl: true,
      }

      response = Net::HTTP.start(uri.hostname, uri.port, options) do |http|
        http.request(request)
      end

      records = JSON.parse(response.body)["records"]
      if records.nil? || records.empty?
        raise "NotFoundError"
      end
      message_body = records[0]["message_body"]

      # SMSの本文から2FAのコードを取得する
      code = message_body[/\d{6}/]
      if code.nil? || code.empty?
        raise "NotFoundError"
      end

      # コードを入力する
      o.puts code
    end

    i.expect(/Pass the following via the FASTLANE_SESSION environment variable:/, 10) do |match|
      raise "UnknownError" unless match
      o.puts "y"
    end

    begin
      while (i.eof? == false)
        puts i.gets
      end
    rescue Errno::EIO
    end
  end
end

ここからは蛇足ですがVonage以外のSMSが受信できるサービスはTwilioClickatellを試しました。

いろいろ試行錯誤しましたが、結論としてはAppleの2FAのSMSを受け取ることはこの2つのサービスでは不可能でした。

電話番号にはLong NumberとShort code(5-6桁の番号)がありAppleの2FAはShort codeから送られるようですが、どうもそれが受信できない模様です。

TwilioはリクエストするとShort codeの受信が有効になるアップグレードがあり、それを有効にして試したものの受信できませんでした。

ということで、いろいろなSMSのサービスを試したところVonageの番号だとAppleの2FAのSMSが受信できた、という話でした。

(別解)Androidデバイスを用いてSMSを転送する

これはまったく試してない方法ですが、AndroidはプログラムからSMSが取得できる手段があるそうです。なので、適当に余っている検証機にSIMを挿して、SMSを適当にGmail等に転送するようなプログラムを設定すればもっと安価に実現できるのかもしれません。

Androidデバイスを用いた方法は下記のコードで実現できるようです。

github.com