14
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Web版 Sign in with Apple を PHP で実装する

Last updated at Posted at 2020-04-19
update: 2020/04/21: 少し表現を修正を行いました。

概要

本記事はWebページ上で Sign in with Apple を使ってサインインを行い、認証情報の取得を行う
なお、作成したコード類は Lumen / PHP で記述しており、個人的興味で作成し、備忘録として残したものである。

Sign in with apple とは

Apple が発表した Apple ID を用いてサインインを行えるようにする機能。
iOS13以降の端末であれば、Face ID, Touch ID での認証に対応している。
また、それ以外の端末及び Web, Andorid では Webview を使った認証に対応している。

iOSアプリの場合は SNSログインを用いている場合は対応を行う必要がある。
※ 投稿時点では 6/30 までに対応する必要があるようです。
(最近社会情勢等でどんどんデットラインのリミットが伸びているようです)

サインインできるようになるまでの概略

Apple と サーバ(App) とのやり取りとしては以下の形になる。
Untitled (2).png

サインインを行った場合にコールバックに設定したURLに対してPOSTメソッドでリクエストを実行する
その際に code, id_token, state がリクエストとして実行される。

  • code: 5分間のみ有効な Authorization Code でこの値を利用して token を利用してAppleからtokenを取得する
  • id_token: JWT(JSON Web Token) 形式でID状態が格納されている。参照できるようにするためにはAppleから取得できる公開鍵を利用してデコードする必要がある
  • state: サインイン実行時に設定した値。CSRFでの検証に利用できる。

Apple から必要な情報を作成する

Sign in with Apple を実装するにはまずサインインを行うために必要な情報を Apple Developer から取得する必要がある。
※ Apple Developer から情報を取得できるようになるために Developer Program に加入する必要があるので、加入しましょう

必要なもの

  • Team ID (Apple Developer Program に加入したあとに発番されるもの、Membership 上で確認可能)
  • Service ID (App ID に紐づく形でWeb上でサインインできるように作成するもの(現状機能がサインイン機能しかないためサインイン専用とみてもいいと思われます。))
  • Key ID (App ID で紐づいた鍵のID)
  • Apple から取得できる p8 形式の秘密鍵 (Key ID に紐づいた秘密鍵)

Service ID の取得
(なお Service ID に必要なApp ID の取得は省略します。)

Certificates, Identifiers & Profilesのページで新規作成を行う。
Identifiersを選択し + を押下すると選択肢が出てきますので AppID を作成している場合は Service IDs を選択する

DescriptionIdentifier (AppIDが com.example であれば com.example.auth のような形で合わせる形が無難)を設定する

Inkedコメント 2020-02-18 232627_LI.jpg

設定後に Sign in with Apple の設定が可能になるため有効化を行い、App ID の紐づけと callback URL の設定を行います。
一回実装しようとしていた 2月当時ではドメインの検証を行う必要がありましたが、現在では不要になっています。

コメント 2020-04-19 184815.jpg

KeyIDと鍵の取得
Certificates, Identifiers & ProfilesからKeysを選択しService IDの作成と同様 + を押下

コメント 2020-04-19 185146.jpg

鍵の名前(管理しやすいもの)とSign in with Apple を有効化し、設定で作成した App IDを指定します。
生成すると秘密鍵のダウンロードとKeyIDが表示されるので秘密鍵をダウンロードする。
※秘密鍵は1度きりのダウンロードになるため、ページを離れるとダウンロードできなくなります。

サインインをできるようにする

まずは、Appleでサインインを行えるように Web上にサインインボタンの設置を行い、ログイン画面を出せるようにする。 Apple側より Sign in with Apple JS のSDKが配布されているため難易度は高くない
https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js

コメント 2020-04-19 181616.jpg

AppleID.auth.init を利用して各種情報 client_id(service_idと同値), redirectURI(登録したコールバックURL), 必要であれば stateを設定する。

scope を設定すれば user, email の情報を取得できるが、これらの情報はApple側で初回認証を行った場合にのみレスポンスに帰ってくるため、2回目以降の情報取得には注意が必要となる。

usePopup を有効にした場合はポップアップで表示してくれる(デフォルトはfalse)

id="appleid-signin" をクリックすることで appleid.auth.js 内で処理が行われサインインページに遷移を行う。

なお自動的にサインインページに遷移させたい場合は AppleID.auth.signIn(); を init のあとに入れることで遷移されるようになる。

<html>
    <head>
      <title>Apple Lumen</title>
      <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
    </head>
    <body>
        <h1>Hello,Lumen</h1>
        <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
        <script>
          AppleID.auth.init({
            clientId : '<?php echo $client_id ?>',
            redirectURI : '<?php echo $redirect_uri ?>',
            state : '<?php echo $state ?>'
          });
        </script>
    </body>
</html>

コメント 2020-04-19 181642.jpg

Authorization Code の検証

Apple の認証を終えてコールバックURLが実行された際に、まず送られてきたリクエストが正しいものかどうかを確認するために検証を行う。

検証を行うために使用するエンドポイントは POST https://appleid.apple.com/auth/token である。
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

以下の情報を上記エンドポイントにリクエストを行う必要がある。

  • code: コールバックURLのリクエスト(code)を使用
  • grant_type: authorization_code (今回は認証のみのためこちらを使用する)
  • redirect_uri: コールバックURL
  • client_id: Service ID
  • client_secret: 生成方法を後述する

実装としては以下の形になる env は lumen を使用しており、環境ごとに変わるため.envに定義する

必要なパラメータを準備し application/x-www-form-urlencoded の形で、curlを実行する

    private $token_validation_link = 'https://appleid.apple.com/auth/token';

    private function verify_token($code){
        try {
            $params = array(
                'code' => $code,
                'grant_type' => 'authorization_code',
                'redirect_uri'	=>  env('REDIRECT_URL', 'callback'),
                'client_id' => env('CLIENT_ID', 'client_id'),
                'client_secret' => $this->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, $this->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) {
                Log::info(curl_error($curl));
                return FALSE;
            }

            curl_close($curl);
            return $response['id_token'];
        } catch (Exception $e) {
            Log::info($e->getMessage());
            return FALSE;
        }
    }

client_secret の作成

token の検証を行う際には JWT でエンコードされたclient_secretを作成する必要がある
エンコードの形式も Apple から取得した秘密鍵を利用して ES256 でエンコードする必要がある

JWTのエンコードを行う前には以下の形式でヘッダとペイロードの作成を行う

{
    "alg": "ES256", // 固定
    "kid": "${KeyID}"
}
{
    "iss": "${TeamID}",
    "iat": unix時間で現時刻,
    "exp": unix時間で有効期限,
    "aud": "https://appleid.apple.com", // 固定
    "sub": "${ServiceID}"
}

JWT のエンコード・デコードには firebase が php 向けに提供しているためこちらを利用する

こちらの情報を元にエンコードを実装したものが下記になる

    use Firebase\JWT\JWT;

    private function create_client_secret()
    {
        $key = file_get_contents(env('PRIVATE_KET_PASS', 'null'));
        $now = time();
        $expire = $now + (7 * 24 * 60 * 60);

        $payload = array(
            'iss' => env('TEAM_ID', 'team_id'),
            'iat' => $now,
            'exp' => $expire,
            'aud' => 'https://appleid.apple.com',
            'sub' =>  env('SERVICE_ID', 'service_id')
        );

        return JWT::encode($payload, $key, 'ES256', env('KEY_ID', 'key_id'));
    }

JWT のデコード

token の検証を終えた後は JWTのデコードを行い、IDの情報を取得する。
まず JWTのデコードに必要な情報を Apple 側から取得する。ただ、レスポンスの結果は公開鍵そのものではない(JSON Web Keyの形式)ので RSAの公開鍵として生成する必要がある。

JSON Web Key から公開鍵を生成するには phpseclib を利用してRSA形式の鍵を生成する
生成の方式は以下を参考に実装をした。

    use phpseclib\Crypt\RSA;
    use phpseclib\Math\BigInteger;

    private 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();
    }

公開鍵を生成したのち、JWTでデコードを行って、デコードが行えた場合はその値を渡すことで情報が取得される。また、Apple から取得できる JSON Web Key は複数個存在するため、それぞれのパターンで検証を行う必要がある。

    use Firebase\JWT\JWT;

    private $get_public_key_link = 'https://appleid.apple.com/auth/keys';

    private function decode_user_token($jwt_token)
    {
        $curl = curl_init($this->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 = $this->create_jwk_public_key($data);
                $token = JWT::decode($jwt_token, $public_key, array('RS256'));
                break;
            } catch (Exception $e) {
                if($data === $last_key) {
                    return null;
                }
            }
        }

        return $token;
    }

最後に

この流れで取得された結果デコードされたデータに sub があるが、その値がAppleから取得できるIDとなり、サインインする度に変更される値ではない。
そのため、subを利用することでサービス内でユーザ情報を特定することが可能になる。

現状、Appleでのサインイン対応は少ないためあまり機能は多くないが、iOS13 以降の端末では、サインインが簡単に行えるようになるため、今後対応されるアプリが増えていくことで、利用できる機能が増えることに期待したい。

参考

https://developer.apple.com/documentation/sign_in_with_apple
https://blog.katsubemakito.net/articles/sign-in-with-apple-js

14
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?