全く知らない人のためのSign in with Apple

Mercari Advent Calendar 2019 の10日目担当は メルカリ iOS チームの @HideakiTouhara です。

はじめに

2019年のWWDCで新しいSign in, Sign up方法であるSign in with Appleが発表されました。
今回は主にクライアントの実装を起点に、その流れを説明していきたいと思います。

Sign in with Appleの説明や特徴

まず、そもそもどういうものなのでしょうか?
2019年9月に更新されたガイドラインを引用します。

Make it easy for users to sign in to your apps and websites using the Apple ID they already have. With privacy and security features built-in, Sign in with Apple is a great way to help users set up an account, sign in, and engage with your app quickly and easily. All accounts are protected with two-factor authentication for superior security, and Apple will not track users’ activity in your app or website.

すでに持っているApple IDを使用して、簡単にログインできるようにするための機能です。
2段階認証によるプロテクトや、Appleがそれを使ってユーザーのトラッキングを行わないというのがメリットのようです。

こちらのWWDC2019 のセッション動画では5つの特徴があると述べられています。(2:30ごろから)

  • Streamlined account setup

すでにAppleアカウントでログイン状態にあれば、特に必要なことなく簡単にログインすることが可能。
これはApple Payの状況と似ているかもしれませんね。Apple Payのシームレスさをサインアップのシーンでも実現しようとしていると感じました。素晴らしい。。。

  • Verified email addresses

すでに認証済みのメールアドレスを使用してのシステムなので、メールアドレス自体の認証ステップが必要なくなります。
ここで “Hide my email” というセクションがあり、ユーザーは自身のメールアドレスではない自動的に作られたものを使用して、サインアップフローを進めることができます。
つまり、ユーザーは本当のメールアドレスをサービス側に伝えることなく、サインアップできます。
開発者はそのprivate emailに対して、何かしらのメッセージを送るには、送り元のメールアドレスをdeveloper siteより設定する必要があります。

f:id:hideakitouhara:20191210172557p:plain

  • Built-in security

すべてのアカウントは2段階認証によって保護されます。加えて、永続的にログインが保持され、再認証はTouch IDもしくは、Face IDで行うことができます。

  • Anti-fraud

機械学習などを用いて、ユーザーが本当の人間かどうかを判別します。

  • Cross-platform

iOS, macOS, iPadOS, watchOS, JavaScriptに対応。
iOSのSDKに加え、Sign in with Apple JSというフレームワークも登場しました。(後述しますが、Androidにも対応することができます。)

といった感じで、サインアップのシームレスな体験と堅牢なセキュリティというのが、アピールポイントのようです。

必須なの?

こちらもガイドライン を引用します。

Apps that exclusively use a third-party or social login service (such as Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon, or WeChat Login) to set up or authenticate the user’s primary account with the app must also offer Sign in with Apple as an equivalent option.

サードパーティ製のソーシャルサービスログインを提供している場合は、Sign in with Appleも同じように提供しなければなりません。
他にもいくつか条件がありますので、引用だけしておきます。

Sign in with Apple is not required if:

Your app exclusively uses your company’s own account setup and sign-in systems.

Your app is an education, enterprise, or business app that requires the user to sign in with an existing education or enterprise account.

Your app uses a government or industry-backed citizen identification system or electronic ID to authenticate users.

Your app is a client for a specific third-party service and users are required to sign in to their mail, social media, or other third-party account directly to access their content.

デッドライン

Existing apps and app updates must follow them by April 2020.

加えて、2020年の4月までに対応することが求められています。これ以降対応していないアプリがどうなるかはわかりません。しかし、特に企業アプリはリジェクトリスクを抱えたまま事業を継続していくのは厳しいと思うので、多くのアプリが対応に向けて動いているのではないかと思います。

実装方法

では、実際にどのようにして実装していくのか手順を見ていきましょう。

以下図がSign in with Appleの実装フローの一例になります。

f:id:hideakitouhara:20191210171611j:plain

ではiOSを例に、コードを書いていきます。

Sign In With Apple Capabilityを追加

Xcode11以上のプロジェクトにおいて、TARGETS -> Signing & CapabilitiesからSign in with AppleのCapabilityを追加します。

Developer siteより登録

f:id:hideakitouhara:20191210172624p:plain

Developer siteよりApp IDの設定を変更する必要があります。

Sign in with Appleをactiveにしましょう。

ここで一点注意すべき点があり、Enterprise版のApp IDではSign in with Appleの設定が存在しません。これはApple Payと同じシチュエーションです。

https://help.apple.com/developer-account/#/dev21218dfd6

なので通常のApple Developer Programで設定を変更する必要があります。

テストなどで、Enterprise版を使用している場合は、この点に注意して、Sign in with Appleの開発、検証の場合のみターゲットを変更するなど対応する必要があります。

ちなみに、Web版やAndroid対応の際に、App IDがなくて、困ることがあるかと思います。

その際に使用するのが、Service IDです。

iOS以外のプラットフォームに対応するときはこのようなリクエストにパラメーターを詰めて、送るのですが、ここで事前にService IDを作成しておく必要があります。

ASAuthorizationAppleIDButtonを追加

続いて、Buttonを追加します。

まずは必要なFrameworkを追加します。

import AuthenticationServices

AuthenticationServicesがSign in with Appleを使用するために必要なフレームワークです。

次にButtonです。

let button = ASAuthorizationAppleIDButton()
button.addTarget(action:@selector(pushApplebutton, for: .touchUpInside)
view.addSubview(button)

Buttonといっていますが、実際はUIControlを継承していました。

ですので、フォントを変えたり、アイコンの画像を加工したりといったことが難しいです。

Sign inのリクエストを送る

リクエストの実装例です。

Buttonをタップしたタイミングで、performRequestsします。

@objc private func pushAppleButton() {
let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = nonce
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}

ASAuthorizationAppleIDProviderはAppleIDをベースにした、認証のためのリクエストを作成するためのクラスです。createRequest()で実際に作成しています。

requestedScopesはユーザーに要求する連絡情報です。emailとfullNameがあります。
続いて、ASAuthorizationControllerの設定を行います。
delegateを設定することで、リクエストの実行結果を受け取る準備ができます。
presentationContextProviderで、認証画面を表示するスクリーンのコンテキストを設定します。
最後はperformRequests()でリクエストを実行します。

ここで、nonceを指定している理由についてですが、nonceはリクエストのオプションパラメーターです。
nonceを使用することで、auth code(認可コード)をリクエストしたときと、ID Token取得までの一連の流れが、同一セッションであることを保証することができます。
オプションですが、使っていきましょう。

リクエストのレスポンスを受け取る

先程、ASAuthorizationControllerを作成し、delegateを設定したことでレスポンスを受け取れるようになりました。

以下がdelegate protocolの定義です。

public protocol ASAuthorizationControllerDelegate : NSObjectProtocol {
optional func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization)
optional func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error)
}

didCompleteWithAuthorizationとdidCompleteWithErrorがあり、その名の通り、成功したときと、失敗したときに対応しています。

まずはdidCompleteWithAuthorizationの方を見ていきます。
ASAuthorizationはproviderとcredentialという2つのパラメーターを持っています。

credentialの方の型定義は以下のようになっています。

public protocol ASAuthorizationCredential : NSCopying, NSSecureCoding, NSObjectProtocol {
}

今回credentialを使うときはASAuthorizationAppleIDCredentialにキャストします。

定義は以下です。

open class ASAuthorizationAppleIDCredential : NSObject, ASAuthorizationCredential {
// 一部省略
/** @abstract A short-lived, one-time valid token that provides proof of authorization to the server component of the app. The authorization code is bound to the specific transaction using the state attribute passed in the authorization request. The server component of the app can validate the code using Apple’s identity service endpoint provided for this purpose.
     */
open var authorizationCode: Data? { get }
// 一部省略
/** @abstract An optional email shared by the user.  This field is populated with a value that the user authorized.
     */
open var email: String? { get }
/** @abstract An optional full name shared by the user.  This field is populated with a value that the user authorized.
     */
open var fullName: PersonNameComponents? { get }

auth codeを取得します。

public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential, let authorizationCode = credential.authorizationCode else { return }
// Do something to use authorizationCode.
}

他にもemailやfullNameなども持っているので、サービスのログインに必要な情報に応じて使い分けてください。

続いて、エラー処理のメソッドです。
こちらは例えば、ユーザーがパスコード入力や、連絡情報編集画面などで、離脱したときに呼ばれます。
errorのパラメータを受け取ることができるので、そのハンドリングをするのが一般的でしょう。

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
showError(error)
}

この手順はフロー図でいうと、Step5の部分になりますが、一点気をつけることがあります。

ここでEmailや名前を取得できると述べていますが、できるのは最初の一回のみです。
これ以降取得できるのはユーザーが連携を切ったときだけです。
なので、それらの情報が必要な場合は、最初にもらったタイミングでどこかに保存する処理を書く必要があります。
加えて、返ってこない前提の処理を書き加える必要があります。
例えば、2回目以降Emailを取得する場合は、ID Tokenを取得する際にEmailが含まれているので、そこで取得することができます。

Apple ID Stateの変更を監視する

また、credentialのstateが変更になったときの監視をすることもできます。

Notificationで行います。

追加

NotificationCenter.default.addObserver(self, selector: #selector(credentialStateChanged), name: ASAuthorizationAppleIDProvider.credentialRevokedNotification, object: nil)

監視

@objc func credentialStateChanged() {
let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: "userId") { (state, error) in
switch state {
case .authorized:
case .notFound:
case .revoked:
}
}
}

それぞれ、authorizedはstateが正しい状態、notFoundはuser IDが見つからない状態、revokedはユーザーによって、取り消しされた状態を表します。

これ以降は自サービスのログインフローに繋げていって、完了という流れになります。

Web, Androidへの対応

もし複数プラットフォームを提供しているサービスにSigin in with Appleを導入する場合、WebやAndroidアプリにもこちらを対応させたいシチュエーションが出てくると思います。

その場合もAppleはSign in with Apple JSというSDKを提供しているので、認証はWebページ上でこことかここ の仕組みを使い、行うことができます。

Webページを開き、アカウント情報を入力してもらうことによって処理を進めます。

他にも、例えばサーバーサイドと通信するために、REST APIが提供されています。

これらの対応は先程のフローとほぼ同じです。

f:id:hideakitouhara:20191210171611j:plain

Step4の部分がiOSの場合はiOSのSDKを利用していたものが、Webページを開いて、行うようになります。
フロー自体は同じで、それらを各プラットフォームで実装していくのみになります。
Buttonの作成などは簡単なので、省きます。

AuthのリクエストはこのAPIを使います。

分かりずらかったパラメーターを解説します。

client_idは先程も紹介したようにService IDです。
こちらはDeveloper siteから作成することができます。

f:id:hideakitouhara:20191210172614p:plain

Sign in with Appleを使用する場合は、いくつかの設定をする必要があります。

まずはPrimary App IDの設定です。

続いて、Web Domainを用意し、該当のパスに指定されたファイルを置かなければなりません。

以上でclient_idの設定は完了です。

今回App ID, Primary App ID, Team ID, Service IDなど、色々出てきて、ややこしかったので、ここでまとめます。

App IDはDeveloper siteから登録できるIdentifierのことです。
Sign in with AppleやApple Payなど、一部サービスによっては、エンタープライズ版とADP(Apple Developer Program)版で制限に差異があります。

このIDを設定して、Sign in with Appleのconfigureを変更するタイミングで、 ”App ID Configuration”の項目が表示されます。
Primary App IDとして使用するか、Group with an existing primary App IDとして使用するかを聞かれます。
後者を選択した場合はどのPrimary App IDと紐付けるかを聞かれます。
Service IDでもどのPrimary App IDと紐付けるかを聞かれます。
つまり、Primary App IDはメインのIDでそれを中心に他のIDが紐づく形になります。

f:id:hideakitouhara:20191210172420j:plain

Web上でサインインする場合は、ユーザーにはPrimary App IDのサービスにログインすることを表示する形になります。
つまり、Primary App IDに紐づくサービスたちはログイン時にPrimary App IDに設定してある情報が表示されるため、複数サービスで命名の統一性を出すことができます。
大規模なIDサービスを持ち、それらを利用し、紐付いている複数のサービスを所有している場合などを使えるかなと思いました。
Team IDはauthのAPI にある、Client Secretにあるissを作成するのに使用します。

パラメーターの紹介に戻ります。

redirect_uriは、認証が完了したタイミングでauth codeとともに、リダイレクトさせるためのURIです。
stateはauth requestのときのセッションが同一がどうかを判断するために使用しています。
nonceとの違いは、どの部分を保証するかという点になります。

という流れで、authを取得したあとは、フロー図のように進めていけば、実装が可能です。

普段iOSを触っていない人からすると、慣れない設定もありそうですね。

最後に

すでにいくつかサードパーティログインシステムが存在しているなかで、ようやくAppleが出してきたのがSign in with Appleです。

個人的には、端末のログインと紐づくことによる簡単さや永続性に惹かれました。

このあたりはApple Payでも感じましたが、余計な認証なく、シームレスにかつ、セキュアというのを実現できるのは端末を作っている会社だからこそだなと感じました。
iPhoneユーザーはこのサービスを使いたがるのではないでしょうか。

マルチプラットフォーム対応の際にiOS SDKに比べると、煩雑である点など、いくつかの改善点があると思いますが、個人的にはスタンダードになっていってほしいなと思えるシステムでした。

明日の執筆担当は、メルカリ iOS チームの@damianさんです。それでは引き続きお楽しみください!