前回のおさらい
前回は「Appleでサインイン」を実装する前の下準備をまとめました。今回は実際にPHPで実装をしたいと思います。
なお、ソーシャルログインについて実はFirebase上でできるぽいのですが、今回はそれを使わずに実装したいと思います。
実装に必要な情報
前回Apple Developersで登録した情報と今回新たに必要な情報をあらかじめまとめます。clientId | Service IDの bundle ID |
---|---|
redirectURI | サインインボタン押下時に戻ってくるURL ここで実際に情報を取得します。 Apple Developersの Service IDに設定したURLとなります (登録したURL以外を指定するとエラーになります) |
証明書 | Keyでダウンロードしたp8形式の証明書 コールバックした情報を証明書で照合する |
scope | どの情報を取得するか(例 name , email) |
state | 符号のようなものでstateの文字列比較で、このサーバーから送信されたか判定する |
KEY ID | Apple DevelopersのKeysを発行した際に付与された文字列 |
TEAM ID | Apple Developersのアカウントに紐付く文字列 |
あらかじめ上記の用意をしておきましょう
PHPで実装してみる
実際にPHPで実装してみましょう。作る画面は2つでボタンを表示する画面とredirectURIで指定した受け取る画面です。
ボタンを表示する画面の箇所を抜き出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?php $client_id = '{ServieIDのbundle ID}'; $redirect_uri = "https://".$_SERVER["SERVER_NAME"]."/callback.php"; $scope = 'name email'; $state = 'hogehoge'; ?> <!doctype html> <html lang="ja"> <head> <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/ja_JP/appleid.auth.js"></script> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ソーシャルログイン</title> </head> <body> <div class="container"> <h1>Appleでサインイン</h1> <div id="appleid-signin" data-mode="center-align" data-border-radius="15" data-width="220" data-height="32" data-color="black" data-border="true" data-type="sign-up"></div> <script> AppleID.auth.init({ clientId : '<?php echo $client_id ?>', redirectURI : '<?php echo $redirect_uri ?>', state : '<?php echo $state ?>', scope : '<?php echo $scope ?>' }); </script> </div> </body> </html> |
こんな感じです。
ボタンは<div id=”apple-signin”>で行います。
data-type=”sign-up” になってますが、ここは[sign-in][sign-up]と選べます。
[sign-up]は画像のようにあり、アカウント作成時に使うボタンです。
[sign-in]は作成したアカウントにログインするときに使うボタンです。
どちらも機能としては変わらないので、適宜使い分けをしてください。
戻り先の処理
戻り先の処理のソースを書く前に、1つ設定する必要がありました・・・composerでライブラリのインストール
「JWT」と「phpseclib」を使うのであらかじめインストールしてください・・・コマンドは
composer require firebase/php-jwt
composer require phpseclib/phpseclib:~2.0
でいけるかと思います。
※ phpseclibのバージョンは現在のところバージョン3なのですが、今回はバージョン2で解説します。
処理の説明
基本的な処理の流れですが- Apple Developersで生成した秘密鍵を元にES256でclient_secretを作成する
- POSTできたcodeからAppleのREST API (/auth/token)でTOKENを受け取る
- 受け取ったTOKENを AppleのREST API(/auth/keys)から取得した公開鍵でデコードしユーザーTOKENを受け取る
- ユーザーTOKENから一位のsub
また、scopeで取得できるnameとemailは初回のみ取得できます。
2回目以降は取得できないので注意してください。
(一応Apple IDのページから「Appleでサインイン」で該当アプリを削除をすると再度取得できます)
実際のソース
最低限の処理で進めています。$subに一意の文字列が来るので、この値と初回のみ取得できるemailやnameを登録していきましょう
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
<?php define("APPLE_CALLBACK_URL", "https://".$_SERVER["SERVER_NAME"]."/callback.php"); define("APPLE_CLIENT_ID", "xxxxxxxx.auth"); define("APPLE_SCOPES", "name email"); define("APPLE_STATE", "blog_sample"); define("APPLE_PRIVATE_KEY_PASS", "/xxxxxxxxx/key/AuthKey_{key_id}.p8"); define("APPLE_KEY_ID", "{key_id}"); define("APPLE_TEAM_ID", "{team_id}"); $client_id = APPLE_CLIENT_ID; $redirect_uri = APPLE_CALLBACK_URL; $scope = APPLE_SCOPES; $state = APPLE_STATE; require_once('/vendor/autoload.php'); use Firebase\JWT\JWT; use Firebase\JWT\Key; use phpseclib\Crypt\RSA; use phpseclib\Math\BigInteger; $state = $_POST["state"] ?? ""; // 初回のみ取得できる $users = array(); $post_users = $_POST["user"] ?? ""; if($users != ""){ $users = json_decode($post_users, true); } if($state == APPLE_STATE){ $code = $_POST["code"] ?? ""; if($code != ""){ $state = $_POST["state"] ?? ""; $id_token = $_POST["id_token"] ?? ""; if(!verify_token($code, $users)){ // codeのバリデーションエラー exit; } }else{ // codeがない exit; } }else{ echo "state 無効"; exit; } // ① POSTできたcodeからAppleのREST API (/auth/token)でTOKENを受け取る function verify_token($code, $users){ $token_validation_link = 'https://appleid.apple.com/auth/token'; $client_id = APPLE_CLIENT_ID; $redirect_uri = APPLE_CALLBACK_URL; try { $params = array( 'code' => $code, 'grant_type' => 'authorization_code', 'redirect_uri' => $redirect_uri, 'client_id' => $client_id, 'client_secret' => create_client_secret() ); $data = http_build_query($params); $header = array( "Content-Type: application/x-www-form-urlencoded", "Content-Length: ".strlen($data), "User-Agent: UA" ); $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_URL, $token_validation_link); curl_setopt($curl, CURLOPT_HTTPHEADER, $header); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); curl_setopt($curl, CURLOPT_TIMEOUT, 5); $result = curl_exec($curl); $response = json_decode($result, true); $status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); if ($status_code !== 200) { // print_r(curl_error($curl)); return FALSE; } curl_close($curl); get_apple_datas($response, $users); return $response["id_token"]; } catch (Exception $e) { // print_r($e->getMessage()); return FALSE; } } // ② Apple Developersで生成した秘密鍵を元にES256でclient_secretを作成する function create_client_secret(){ $key = file_get_contents(APPLE_PRIVATE_KEY_PASS); $now = time(); $expire = $now + (7 * 24 * 60 * 60); $payload = array( 'iss' => APPLE_TEAM_ID, 'iat' => $now, 'exp' => $expire, 'aud' => 'https://appleid.apple.com', 'sub' => APPLE_CLIENT_ID ); return JWT::encode($payload, $key, 'ES256', APPLE_KEY_ID); } function decode_user_token($jwt_token) { $get_public_key_link = 'https://appleid.apple.com/auth/keys'; $curl = curl_init($get_public_key_link); curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($curl); $info = curl_getinfo($curl); curl_close($curl); if ($info['http_code'] != 200) { return null; } $response = json_decode($response, true); $public_keys = $response['keys']; if ($public_keys === null) { return null; } $last_key = end($public_keys); foreach($public_keys as $data) { try { // decode action $public_key = create_jwk_public_key($data); $token = JWT::decode($jwt_token, new Key($public_key, 'RS256')); break; } catch (Exception $e) { if($data === $last_key) { return null; } } } return $token; } // 公開鍵の生成 function create_jwk_public_key($jwk){ $rsa = new RSA(); $rsa->loadKey( [ 'e' => new BigInteger(JWT::urlsafeB64Decode($jwk['e']), 256), 'n' => new BigInteger(JWT::urlsafeB64Decode($jwk['n']), 256) ] ); $rsa->setPublicKey(); return $rsa->getPublicKey(); } // 取得したデータから情報を取得 function get_apple_datas($response, $users){ $id_token = $response["id_token"]; $token = decode_user_token($id_token); // ユーザー識別コード $sub = $token->sub; echo "sub:".$sub."<br />"; // 初回時のみ取得可能 if($users){ $name = $uses["name"]; $firstName = $name["firstName"]; $lastName = $name["lastName"]; $email = $uses["email"]; } } |
「iCloud+」ユーザーは初回アクセス時に「メールを共有」か「メールを非公開」にするか選択でき、非公開にすると、取得できるのは独自のランダムなメールアドレスとなり、前回のエントリーで設定した認証されたドメイン(+サーバー)からしか送信が行えません。
ですので、ユーザーにメールを送信する場合は必ず設定を行いましょう。
最後に
Apple Developerでの設定は少し大変ですが、ライブラリを使えば割と簡単です。とはいえ、phpseclibがバージョン3使えないのが気になるので、そこら辺は調べていきたいと思います。