みなさん初めまして、サーバーサイドエンジニアの田宮(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を作成します。
# 手順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を作成します。
# 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用のテーブルを新しいユーザー識別子で更新します。
# ユーザー識別子とアプリユーザー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では新技術や技術的なノウハウなどの記事を定期的に更新していきますので皆さんお楽しみに!