概要
アプリが、サードパーティのログインサービスを利用してログインを実装する場合、Appleでサインイン(Sign in with Apple)の実装が必須になりました。
通常はAuthenticationServicesに含まれているAPIでSign in with Appleの処理が可能ですが、Sign in with Apple関連のAPIはiOS 13以上の対応になるので、iOS 12では別な手段を講じなければいけません。
その手段について説明します。
他プラットフォームでも一部は共通する手段になるかと思います。
必要なもの
以下の実装はUniversal Links対応されていることを想定しています。
App Developer Programで必要な作業を行う
Service IDの追加
以下の手順を参考に、Services IDsの追加を行います。
App Developer Programの構成は更新される可能性があるので参考程度にしてください。
- identifierを選択し、+ボタンを選択
- Services IDsをチェックし、continue
- Description、Identifierを入力して、continue
- Descriptionは任意の値
- Identifierは、一意なIDにする(例:com.example)
- 先ほど作成したService IDを選ぶ
- Sign In with Appleにチェックを入れてConfigure
- Web Authentication Configurationを入力
- Primary App IDに、利用するアプリを選択
- Domains and Subdomainsは、Universal Linksに設定しているドメインを指定 (例:example.com)
- Return URLsに、Universal Linksが起動してアプリが立ち上がるURLを指定 (例:https://example.com/callback)
- このコールバックURLは、それぞれのプラットフォームの起動に使う想定です
- Description、Identifierを入力して、continue
サインインするためのボタン画像を用意
サインインするための公式のボタンは、iOS 13以上であれば ASAuthorizationAppleIDButton
などを使えばいいですが、iOS 12以下の場合はどちらかで画像を取得する必要があります。
- button API(appleid.cdn-apple.com/appleid/button)から取得
- 画像リソースをダウンロードして使う
ボタンを押して、Webブラウザ経由でサインインを行う
ボタンを押したときに、iOS 13であればアプリ上でSign in with Appleを行いますが、iOS 12以下の場合、Web上でのサインインが必要になります。
サインインを行うには、 https://appleid.apple.com/auth/authorize
に対して各種パラメータをクエリとしてつけて、そのURLをUIApplication.shared.open
で外部ブラウザで開くようにします。
var components = URLComponents(string: "https://appleid.apple.com/auth/authorize")
components?.queryItems = [
URLQueryItem(name: "client_id", value: "com.example"),// Service IDsで作成したIdentifier
URLQueryItem(name: "response_type", value: "id_token"),// code or id_token
URLQueryItem(name: "redirect_uri", value: "https://example.com/callback"),// Return URLsの値
URLQueryItem(name: "state", value: state), // 後述
URLQueryItem(name: "nonce", value: nonce) // 後述
]
if let url = components?.url {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
パラメータについての詳しい説明はこちら
これで、外部ブラウザ(iOS 12の場合はSafari)が開き、Appleのサインイン画面が開きます。
アプリ内WebViewを使わない理由
アプリ内WebViewを使わず、Safariを使う理由として、アプリ内WebViewは開発者側が悪意を持っていれば情報を盗むことが可能だからです。
過去にはApple IDを盗んで、その開発者のアプリを譲渡させて自分のものにしてしまう事件も起きました。
このようなことをアプリがやっていない証明として、Safariへ一度認証用のサイトを表示させ、コールバック経由のUniversal Linksからアプリを開かせるようにします。
アプリがアプリ内WebViewを使って認証を行っているかどうかは利用者としても気をつけるべきところです。
詳しい話はこちら。
https://reliphone.jp/notes4/
stateについて
stateは、アプリでサインインボタンを押した人と、SafariでAppleのサインイン画面でサインインした人が一致するかを確かめるために使います。
stateのチェックがないと、攻撃者がコールバックURLをユーザーに送りつけて、ユーザーはそれをクリックして攻撃者としてサインインしてしまい、アプリ内で購入した通貨や商品が攻撃者のアカウントに渡ることになります。
それを防ぐためのものになります。
stateは UUID().uuidString
などで生成するのがいいと思います。
nonceについて
サーバーとの連携の場合、Sign in with Appleでサインインした時のid_tokenをサーバーへ送信してサーバーが処理を行うケースが考えられますが、このid_tokenを再度送った場合、全く同じようにサーバーが処理をしてしまう可能性があります。
生成したnonceをSign in with Apple時にパラメータに渡すと、返されるid_tokenにnonceが付与されます。
これにより、一度処理をしたことがあるid_tokenの場合は処理を行わない、という対応が可能になります。
nonceの生成パターンとして、サーバー側が生成するかアプリ側が生成するかを選択できます。
- サーバー側がnonceを生成し、アプリがサーバーからnonceを取得しSign in with Appleを行い、id_tokenをサーバーに渡す。サーバーはid_tokenをチェックして処理を行う。
- アプリがnonceを生成し、ハッシュ化したnonceをSign in with Appleに渡す。id_tokenが渡ってきたら、サーバーにハッシュ化前のnonceとともにid_tokenを送付し、サーバーはハッシュ前のnonceとid_tokenをチェックして処理を行う。
サインインのコールバックを受けて、アプリがサインインを処理する
SafariでSign in with Appleが成功すると、コールバックURLを呼び出し、Universal Linksでアプリが起動することになります。
その際のエントリーポイントは、AppDelegateの application(_:continue:restorationHandler:)
になります。
userActivity.webpageURL
を取得して、queryを確認します。
前出のstateのチェックをまず行います。
webpageURLに含まれるstateが、サインイン時のStateと同一かどうかを確認します。
正しくサインインされている場合はid_token
が含まれているので、iOS 13と同様のサインイン処理を行います。
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let url = userActivity.webpageURL else { return false }
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems
let state = queryItems?.first(where: { $0.name == "state" })?.value
let idToken = queryItems?.first(where: { $0.name == "id_token" })?.value
// 以下、stateの確認とidTokenの処理を行う
return true
}
以上で、iOS 13と同様のSign in with Appleの機能を実装できるようになりました。