今年のWWDCで発表された「Sign In with Apple」のWeb版(JavaScript版)を実装してみたいと思います。
今回、裏側はFirebaseを利用していますが、Authentication
は使っていませんので、一般的な環境でも参考になると思います。(執筆時点でまだFirebaseが未対応なので使いたくても使えないわけですがw)
※2019-09-23 「1.5 ユーザーを一意に特定するID」追記
AppleIDでログイン
サンプル
以下のページで実際の動作を確認できます。 miku3.net
- PC、スマホ(iOS,Android)ともに動作します。
- 2段階認証(多要素認証)を設定している場合はID/PWでログイン後に聞かれることになります。
- このサンプルではログイン情報の記録はしていません。
ソースコード
index.html
最初のページです。ログイン用のボタンが表示されます。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Test for 「Sign In with Apple」</title> </head> <body> <!-- ボタン --> <div id="appleid-signin" data-color="white" data-border="true" data-type="sign in"> </div> <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script> <script type="text/javascript"> // 本来はCSRF対策用の文字列を準備してください const rand = 'ST' + Math.ceil( Math.random() * 100000 ); // ログイン処理 AppleID.auth.init({ clientId : "net.miku3.id.apple.service", scope : "email", redirectURI: "https://miku3.net/appleid/callback", state : rand }); </script> </body> </html>
AppleID.auth.init()
に渡す引数はそれぞれ以下の通り。
項目 | 説明 |
---|---|
clientId | AppleのDeveloperサイトでIdentifier作成時に設定した値をそのまま書きます(後述) |
scope | 取得したいユーザー情報を記述します。ユーザー名とメールアドレスの指定が可能ですがどちらもWebからだと現状取得できません。 |
redirectURI | ログイン後に戻ってくるURIを記述します。Developerサイトへ事前に登録しておく必要があります(後述) |
state | CSRF対策用の文字列を指定します(後述) |
またボタンの表示で利用する属性の内容は以下の通り。
属性 | 説明 |
---|---|
data-color | ボタンの背景色を指定。black または white |
data-border | 枠線を表示するかをBooleanで指定。 |
data-type | ボタンの種類を指定。sign in , sign up , continue , apple の中から選択 |
ボタンの大きさはCSSで変更可能です。
.signin-button { width: 210px; height: 40px; }
デザイン周りの詳細はAppleのガイドラインを参照してください。
callback
無事にログインした後に表示されるページです。
ログイン時にredirectURI
で指定したURLに、POSTメソッドで戻ってきます。今回はFirebaseのCloudFunctions(Node.js)を利用していますが、Appleから送られてくるクエリーの値をJSON風に表示しているだけです。詳しくは「ユーザーを一意に特定するID」の段落で説明しますが、ユーザーIDを取り出すためにjsonwebtoken
モジュールを利用しています。
実際に利用する場合はログイン時に渡したstate
の値がreq.body.state
にそのまま入っていますので必ずチェックを行ってください。またユーザーIDはdecoded.sub
で取得することができます。
const functions = require("firebase-functions"); exports.appleidCallBack = functions.https.onRequest( (req, res) => { const jwt = require("jsonwebtoken"); const buff = ( req.method === "POST" )? req.body:req.query; // このあたりでCSRF対策用の文字列(req.body.state)のチェック // id_tokenをデコード const decoded = jwt.decode(buff.id_token); res.send( "<h1>Wellcome Back</h1>" + "<h3>Appleから渡される値</h3>" + "<pre style=\"border:1px solid gray;\">" + JSON.stringify(str, null, 4) + "</pre>" + "<h3>id_tokenをデコード</h3>" + "<pre style=\"border:1px solid gray;\">" + JSON.stringify(decoded, null, 4) + "</pre>" + "<hr>" + "ver.0.0.2" ); });
Firebase.jsonでrewrites
の部分を以下のように書き足しています。これで/appleid/callback
へリクエストが来ると、上記のCloudFunctionsが実行されるようになります。
{ "hosting": { "public": "public", "rewrites": [ { "source": "/appleid/callback", "function": "appleidCallBack" }], "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ] } }
CloudFunctionsを独自ドメインで利用する方法については以下の記事を参照ください。 blog.katsubemakito.net
CSRF対策
stateで指定する値は、このページを識別するためのコードと、セッションIDなどを組み合わせた物にしておけば良いと思います。
PHPで書くなら以下のような感じ。唐突なPHPw!
// このページのCDを定義 define('PAGE_CD', 'AUTH-APPLE'); // セッション開始 session_start(); /** * Stateの値を生成 */ function getState(){ $page_cd = PAGE_CD; // ページを識別するためのCD $sess_id = session_id(); // 本人を識別するためのID return(sha1($page_cd . $sess_id)); } /** * Stateの値をチェック */ function checkState(){ $page_cd = PAGE_CD;; // ページを識別するためのCD $sess_id = session_id(); // 本人を識別するためのID $state = $_POST['state']; // Appleから渡されたstate return( sha1($page_cd . $sess_id) === $state ); }
index.html
でgetState()
で生成した値を渡し、redirectURI
で指定したURIのページに戻ってきた際にcheckState()
でチェックするイメージです。
Appleから渡される値
redirectURI
で指定したURIにPOSTメソッド戻ってくるわけですが、このときに以下のような値が渡されます。
クエリー名 | 説明 |
---|---|
state | AppleID.auth.init() で渡したstate の値がそのまま入っています |
code | 5分間有効なシングルサインオン用の値 |
id_token | ユーザーを識別するための文字列。JWT(JSON Web Token) |
scope
でemail
を指定しましたがガン無視されますw 現状だとWeb版はメールアドレスはおろかユーザー名さえも取得できないみたいですね。NDKだとこのあたりうまく行くようなので、どうしてもこれらの情報がほしい場合はiOS用のネイティブアプリを利用することになりそうです。
ユーザーを一意に特定するID
2019-09-23追記
ログインする度にユーザーを特定するID的な文字列はid_token
の中に含まれていますが、JWT形式で渡されるため、そのままでは利用できません。
JWTの詳しい説明は端折りますが、書式としては2つのドットで全体が3つに区切られてまして、それぞれ次を意味しています。結論から言うとこのペイロード内にあるsub
がユーザーIDにあたります。
- ヘッダー(署名のアルゴリズム)
- ペイロード(実際の値が入っている部分。JSON)
- 署名(改ざんの検証に利用)
具体的な処理としては、最初にドット(.)で文字列id_tokenを分割します。ヘッダーとペイロード部分がBase64でエンコードされているので、ペイロードを添付ファイルのようにデコードするとJSON文字列が現れます。このJSONの中の"sub"の部分がユーザー毎に割り振られた不変の値(ID)になるのでこいつを取り出します。
以下は実際のid_tokenを役割ごとに色分けした物です。このうち真ん中の薄い水色のペイロードを取り出します。
取り出したペイロードの部分をBASE64でデコードするとJSONが登場します。
$ echo "eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoibmV0Lm1pa3UzLmlkLmFwcGxlLnNlcnZpY2UiLCJleHAiOjE1Njg5Nzg5NTMsImlhdCI6MTU2ODk3ODM1Mywic3ViIjoiMDAxNDg0LjcwOTk5Mjg3MjE1YzQ2MDA4NmE3ODZjN2JkNjY0MGVmLjExMjkiLCJjX2hhc2giOiJjQUo4di0weDg3TTJxZzhJSFBGRExRIiwiYXV0aF90aW1lIjoxNTY4OTc4MzUzfQ" | base64 -D {"iss":"https://appleid.apple.com","aud":"net.miku3.id.apple.service","exp":1568978953,"iat":1568978353,"sub":"001484.70999287215c460086a786c7bd6640ef.1129","c_hash":"cAJ8v-0x87M2qg8IHPFDLQ","auth_time":1568978353
この"sub":"001484.70999287215c460086a786c7bd6640ef.1129"
がユーザーごとに割り振られた不変のIDとなります。
実際にコードで書くと以下のような処理になります。
<?php // ドットで分割 $jwt = explode('.', $_POST['id_token']); // 0:ヘッダー, 1:ペイロード, 2:署名 // ペイロード部分をBase64デコード $str = base64_decode($jwt[1]); // $strは単なる文字列なので、PHP上で扱いやすくする $payload = json_decode($str); // ユーザー固有のIDを取り出す echo $payload['sub'];
Appleから公開されている仕様はこのあたりを参照してください。 developer.apple.com
ただ上記ドキュメントのsub
部分を読むと何言ってんだこいつ感があるので、OpenID Connectの仕様書を合わせて読むのが良さそうです。
openid.net
Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client
ここでは自前で実装しましたが、実際にはJWTを扱うライブラリが言語毎に存在していますので、そちらを利用するのがお手軽だと思います。改ざんのチェック(verify)なども行ってくれますしね。
その他の言語もJWTの公式サイトの上部メニュー「Libraries」から探すことができます。
Appleサイトでの準備
デベロッパー登録
詳細は割愛しますがAppleへデベロッパー登録を行う必要があります。クレジットカードが必要で毎年11,800円かかります(執筆時点)。ついでにiOSやmacOS用アプリの開発や配信も行えますので、興味があればどうぞw developer.apple.com
昔は1万円未満だったんですけど、徐々に高くなってますね…。個人だと微妙にウッとなるお値段ですw
App IDの作成
最初にこれから「アプリ」を作るぞ!とAppleに意思表示をする必要があります。今回はWebサイトでログインさせるだけですので、ここでいう「アプリ」は概念的な物だと思ってください。
まずはDeveloperサイトへログインします。 developer.apple.com
ログイン後に左メニューの「Certificates, Identifiers & Profiles」をクリック。
どんなアプリを作るか入力します。
- Platformは「iOS」
- Descriptionは説明文になりますので自分が後から見てわかる文字列であれば何でも良いです。
- BundleIDはこのアプリを識別するためのユニークなIDです。URLのドメインを反対にしたような文字列を記述します。URLではありませんので存在しないURLのような文字列で構いません。なお世界中の全アプリからユニークである必要があるので通常は自分が所有しているドメイン名を利用します。
まだ次のページに進んではなりません。 ページ下部にある以下の「Sign In with Apple」にチェックをした後に、ページ上部にある青い「Continue」ボタンをクリックします。横にある「Edit」ボタンはクリックしなくてOKです。
確認画面を挟んで最終的に以下のように一覧に表示されていれば成功です。
Identifierの作成
次に「アプリ」で利用する機能の詳細な設定を行います。 先ほどの画面で「Identifiers」の横にある「+」ボタンをクリックします。
「Services IDs」にチェックし「Continue」ボタンをクリック。
ここから具体的な設定項目に入ります。
- Descriptionは説明文になりますので自分が後から見てわかる文字列であれば何でも良いです。
- Identireはこの設定を識別するためのユニークなIDです。BundleIDと同様にドメインを反対から書いたような書式になりますが、BundleIDとの重複は許されません。index.htmlの
clientId
にはこの文字列を指定します。 - 「Sign In with Apple」の横にあるEnableのチェックボックスにチェック
お次は横にある「Edit」ボタンをクリック、以下のような設定画面が開きます。
- Primary App IDは先ほど作成した「アプリ」を選択します。一つしか無い場合は最初から選ばれています。
- Domainsにはログインページと
redirectURI
で指定するページが存在するドメインを入力します。これは複数入力できない点に注意が必要です。 - ReturnURLsには
redirectURI
で指定するURLを指定します。ここに登録していないURLをredirectURI
で指定するとエラーとなります。
登録が終わったら「Download」ボタンを押して、指定したドメインの所有者であることを証明するために必要なファイル「apple-developer-domain-association.txt」をダウンロードします。これは後ほど利用します。
あとは画面の指示に従い、最終的に一覧画面に登録したIdentifierが登場すれば成功です。
ドメイン認証
最後にIdentifier
の設定で登録したドメインが自分の物であることを証明するために、先ほどダウンロードした「apple-developer-domain-association.txt」を所定の場所にアップロードします。
アップする場所はIdentifier
の設定画面で表示されている以下。
ドキュメントルートに.well-known
というディレクトリを作成し、その中にダウンロードしてきた「apple-developer-domain-association.txt」を放り込みます。ディレクトリ名の頭に「.」を付けることをお忘れなく。
以上で登録作業は完了です。
雑感
ログインさせるだけであれば非常に簡単ですね。ただメールアドレスなどの情報が取れないのは正式版では解決するのかな…。あとWeb上でサンプルをあまり見かけないのはAppleへのみかじめ料のせいですかねw
恐らく秋ごろには正式に始まると思いますので、そのあたりになったら実際のサービスへ組み込んでみたいと思います。
参考ページ
Apple
- https://developer.apple.com/sign-in-with-apple/
- https://developer.apple.com/documentation/signinwithapplejs
- https://developer.apple.com/documentation/signinwithapplejs/configuring_your_webpage_for_sign_in_with_apple
- https://developer.apple.com/documentation/signinwithapplejs/displaying_and_configuring_sign_in_with_apple_buttons
- https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens