オルトプラスエンジニアの日常をお伝えします!

iOSアプリの移管方法:SignInWithApple編

みなさん初めまして、サーバーサイドエンジニアの田宮(id:daiki-tamiya)です。

諸事情により更新を停止していたテックブログですが、約3年ぶりに更新を再開することになりました!今後は順次記事投稿をしていく予定です。

再開後の第一弾として SignInWithAppleによるログイン認証を使用したiOSアプリの移管方法 についてお話ししたいと思います。

はじめに

記事として取りまとめた経緯としましては
・SignWithAppleを導入したアプリの移管事例が少ない
・国内で記事として取りまとめているサイトが少ない
という点から今後需要が出てきそうなので取りまとめることにしました。

また、今回初めてSignWithAppleを導入したアプリの移管検証を行ったのでその際にハマりやすいポイントや気を付けることなども合わせて記載しておりますのでチェックして見て下さい!

※ Firebase AuthenticationによるSignInWithApple認証はメールアドレス管理のため
  特にこちらの記事の対応は必要ないので割愛します。


SignWithApple導入アプリを移管すると何が変わるのか?

基本的にiOSアプリは「AppleDeveloperアカウントA 」から 「AppleDeveloperアカウントB」にアプリを譲渡しても問題無くアプリを引き続き使用できます。ただ、移管した際にSignInWithAppleで使用している一部パラメータに変更が掛かります。

それが

ASAuthorizationAppleIDCredential

ASAuthorizationAppleIDCredential 
var user: String // これ

ユーザー識別子(user)はTeamIDに紐づいたパラメータとなるため、アプリ移管によってTeamIDが変わると次回SignInWithAppleによるログイン時にユーザー識別子は別の値で返ってきます。

つまり、サーバー側でユーザー識別子を保持している場合にアプリ側とサーバー側で異なるユーザー識別子となるため更新する作業が必要となります。


アプリ移管手順

これから説明する移管手順はAppleの公式ドキュメントに沿って対応しています。

SignWithApple Transfers Across Teams
- bringing_new_apps_and_users_into_your_team
- transferring_your_apps_and_users_to_another_team

手順1. client_secret作成
手順2. access_tokenを作成
手順3. 各ユーザーごとにTransfer IDを作成
手順4. 移管後に使用するユーザー識別子をAppleから取得
手順5. ユーザー識別子のCredentialStateの値を確認して再ログインさせる

こちらの手順はアプリ移管後に実行する必要があります。 ただし、手順内で記載されているバッチファイルやアプリ側の改修は事前に準備するようにして下さい。
それでは各手順についての説明していきます。


手順1. client_secret作成

access_tokenを作成するため、まずは取得に必要なclient_secretを作成します。

Generate and Validate Tokens

# 手順1~3で使用する定義
require 'net/http'
require 'json'
require 'jwt'
require 'time'
require 'openssl'

# create client_secret
rsa_private = OpenSSL::PKey::EC.new(File.read(P8_FILE_NAME))
payload = {
    iss: '移管先のTeam ID',
    iat: Time.now.to_i,
    exp: (Time.now + (5 * 60)).to_i,
    aud: 'https://appleid.apple.com',
    sub: 'アプリのバンドルID'
}
header = {kid: '移管先のKey ID'}
client_secret = JWT.encode(payload, rsa_private, 'ES256', header)

ここでハマりやすいポイントとしては「移管先のKey ID」となります。
iOSアプリを譲渡すると
「BundleIdentifier」はそのまま移管先AppleDeveloperアカウントに移行されますが
「SignWithAppleで使用するKey」は移管されません。

アプリ移管後に改めてKeyを作成するようにして下さい。
Key作成方法についてはこちら:「Appleでサインイン」の秘密鍵を作成する


手順2. access_tokenを作成

client_secretを使用して、access_tokenを作成します。

TokenResponse

# request access_token
uri = URI.parse('https://appleid.apple.com/auth/token')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
params = URI.encode_www_form({
    grant_type: 'client_credentials',
    scope: 'user.migration',
    client_id: 'アプリのバンドルID',
    client_secret: client_secret
})
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
res = http.post(uri.path, params, headers)
token = JSON.parse(res.body)['access_token']

手順3. 各ユーザーごとにTransfer IDを作成

access_tokenを使用して、各ユーザーごとにAppleからTransfer IDを取得します。
Transfer IDとは、アプリ譲渡後に使用するユーザー識別子を作成するためのIDです。

今回使用するアプリユーザーIDはテスト用のjsonファイルに用意してから取得していますが、実際はSignInWithApple用のテーブルから取得します。

Generate the Transfer Identifier

# アプリユーザーIDの情報を入れたファイルを用意
users = JSON.load(File.read('data.json'))

# request transfer_sub
uri = URI.parse('https://appleid.apple.com/auth/usermigrationinfo')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    Authorization: "Bearer #{token}"
}
tsubs = users.map do |user|
    params = URI.encode_www_form({
        sub: user[5], # Appleから取得したユーザーID
        target: '移管先Team ID',
        client_id: 'アプリのバンドルID',
        client_secret: client_secret
    })
  res = http.post(uri.path, params, headers)
  data = JSON.parse(res.body)
  [user[0], data['transfer_sub']] 
end

# アプリユーザーIDとユーザー識別子をjsonファイルに保存
File.open('./subs.json', 'w') do |w|
    w << tsubs.to_json
end

手順4. 移管後に使用するユーザー識別子をAppleから取得

Transfer IDを使用してAppleからユーザーIDを取得します。
実際は、ユーザー識別子を取得した後に、DBのSignInWithApple用のテーブルを新しいユーザー識別子で更新します。

Exchange Identifiers

# ユーザー識別子とアプリユーザーIDを取得
subs = JSON.load(File.read('./subs.json'))

# request access_token
uri = URI.parse('https://appleid.apple.com/auth/token')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
params = URI.encode_www_form({
    grant_type: 'client_credentials',
    scope: 'user.migration',
    client_id: 'アプリのバンドルID',
    client_secret: client_secret
})
res = http.post(uri.path, params, headers)
token = JSON.parse(res.body)['access_token']

# request user_id
uri = URI.parse('https://appleid.apple.com/auth/usermigrationinfo')
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    Authorization: "Bearer #{token}"
}
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
users = subs.map do |sub|
  params = URI.encode_www_form({
        transfer_sub: sub[1], # Appleから取得したTransfer ID
        client_id: 'アプリのバンドルID',
        client_secret: client_secret
  })
  res = http.post(uri.path, params, headers)
  uid = JSON.parse(res.body)['sub']
  [sub[0], uid] #アプリユーザーID、ユーザー識別子
end

手順5. ユーザー識別子のCredentialStateの値を確認して再ログインさせる

最後にアプリ側で移管後のユーザー識別子を取得するためにSignWithAppleで再ログインさせる必要があります。 CredentialStateの値に応じて再ログインさせ最新のユーザー識別子を利用するように設計して下さい。

指定 CredentialState
移管前のユーザー識別子(userID)が指定された場合 .transferred
移管後のユーザー識別子(userID)が指定された場合 .authorized
ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userID, completion: {
    credentialState, error in
   
    switch(credentialState){

    case .authorized:
    // 移管済みのユーザー識別子はここに入ります
    break

    case .transferred:
    // 移管前のユーザー識別子はここに入ります(再ログインさせる)
    break

    default:
    break
    }
})

再ログイン後のユーザー識別子について1点だけ気を付けることがあります。
それがアプリ譲渡後すぐに再ログインした場合、移管前のユーザー識別子(userID)が返ってきます。

なので、再ログインのループに入ってしまうためアプリによってはメンテナンス等を挟んでユーザー識別子が移管後のものになったのを確認してからサービスを再開するようにして下さい。
今は早く反映されるのかもしれませんが検証時は8時間くらい経ってから新しいユーザー識別子が返ってきましたので余裕を持たせた方が良さそうです・・!
(検証時:2020年 8月頃)

以上でアプリ移管対応は完了となります。


まとめ

  • アプリ譲渡前、譲渡後でユーザー識別子が変わる
  • サーバー側でアプリ譲渡前のユーザー識別子を保持している場合は、バッチファイル等を用意してアプリ譲渡後のユーザー識別子に更新する必要がある
  • アプリ側はSignWithAppleによる再ログインでユーザー識別子をアプリ譲渡後のユーザー識別子に更新する必要がある

おわりに

久しぶりの再開となり第1弾としてはアプリ移管のノウハウを記事とさせていただきました。 今後もaltplus Tech Blogでは新技術や技術的なノウハウなどの記事を定期的に更新していきますので皆さんお楽しみに!