[PHP] Sign in with Appleの対応をした話

2022年7月7日木曜日

OAuth PHP Sign in with Apple

t f B! P L

# 対応した経緯

現在私は少し年季の入ったWebシステムの保守業務を行なっています。
iOS向けのスマートフォンアプリもあり長年アップデートされていませんでしたが、改修の関係でアップデートの必要がありました。
FacebookやGoogleなどのソーシャルアカウントでのOAuthログインを可能にしていた関係もあり、
AppleIDでのOAuthログインも追加する必要がありました。
ガイドラインの4.8に記載されています。)
それに伴いWeb側もAppleIDでのOAuthログインを可能にする必要性がでたという経緯です。
ガイドラインに違反しているのでアプリの承認がおりないんですよね。

# Sign in with Appleする上で必要なもの

AppleDeveloperで予め取得しておくものを列挙します。
取得方法に関しては、公式ドキュメントを参考に進めてください。

Web向けに「Appleでサインイン」を設定するを参考に進めて以下を取得しましょう。

  • サービスID
  • チームID

「Appleでサインイン」の秘密鍵を作成するを参考に進めて以下を取得しましょう。

  • キーID
  • 秘密鍵

# 処理イメージ

今回訳あってAppleが提供するJSライブラリは使用しません。JSライブラリを使用できるのであれば、利用した方が導入しやすそうな印象でした。

# 認可要求API (公式ドキュメント)

認可要求APIにGETでリクエストします。
リクエストしたパラメータに問題ない場合、Appleのサインインページが表示されます。
AppleIDでログインし、2要素認証コードを入力など諸々すすめると、リクエストパラメータに指定したリダイレクトURIの指定ページへGETかPOSTで戻ってきます。

  • リクエストURL
    • https://appleid.apple.com/auth/authorize

  • リクエストパラメータ
  • 必須 client_id アプリの識別子を指定(ServiceID, AppID)
    必須 redirect_uri Appleサインイン後にリダイレクトするURIを指定
    response_type レスポンスの内容を指定します。
    codeとid_tokenを指定可能です。
    両方指定する場合は、「code id_token」のように指定
    scope レスポンスにメールアドレスと名前を含めるか指定します。
    emailとnameを指定可能です。
    両方指定する場合は、「email name」のように指定
    response_mode レスポンスのモードを指定します。
    query、fragment、form_postを指定可能です。
    scopeを指定した場合は、form_postのみ指定できます
    他にもパラメータがありますが、未使用のため割愛します

  • レスポンスパラメータ
    code 5分間有効な使い捨て認証コード
    id_token ユーザのIDを含む情報。JSON Web Token
    user scopeプロパティで要求したデータのJSON文字列。
    名前とメールアドレス以外の情報取得はできなさそう

    {
        "name": {
          "firstName": string,
          "lastName": string
        },
        "email": string
    }
    

開発中パラメータを変更して何度もリクエストすることになるかと思います。
Apple管理サイトにログインするとアプリケーションとの紐付けを解除できるのでパラメータを変更する場合はこちらを利用しましょう。
また短時間に何度も2要素認証を行うと、制限回数を超えたとかでSMS認証通知を制限されることがありました。
制限解除まで8時間以上かかるようなので気をつけましょう。

# トークン(リフレッシュトークン)生成と検証API (公式ドキュメント)

トークン(リフレッシュトークン)生成と検証APIにPOSTでリクエストします。
リクエストする際のパラメータにclient secretを指定する必要があり、生成する必要があります。

  • リクエストURL
    • https://appleid.apple.com/auth/token

  • リクエストパラメータ
    必須 client_id アプリの識別子を指定(ServiceID, AppID)
    必須 client_secret JSON Web Tokenを別途生成して指定する 
    (composerでJWTライブラリ入れて対応。後述する。)
    必須 grant_type 「authorization_code」を指定する。
    リフレッシュトークンの場合は「refresh_token」を指定する。
    - code 送信された認証コードを指定する。
    - redirect_uri 処理完了後にリダイレクトするURIを指定する。
    - refresh_token リフレッシュトークンの場合は指定する。
    リフレッシュトークンはトークン生成する時のレスポンスパラメータ。

      Header
      {
          alg: ES256,
          kid: キーID (10桁)
      }
      Payload
      {
          iss: チームID (10桁),
          iat: client_secretを生成した時刻(現在時刻をUnixタイム),
          exp: 現在時刻+有効時間, Maxは+15777000 == 6ヶ月,
          aud: "https://appleid.apple.com",
          sub: サービスID
      }
    

  • レスポンスパラメータ
    access_token アクセストークン
    token_type トークンタイプ
    expires_in 有効期限
    refresh_token リフレッシュトークン
    id_token IDトークン。
    複合化するとsubがあり、今後Appleへログインする際に必要な値となる。
    DBへ保存しておく。

# 認証キー取得API (公式ドキュメント)

IDトークンをデコードするための公開鍵を生成する認証キーをGETで取得します。

  • リクエストURL
    • https://appleid.apple.com/auth/keys

  • リクエストパラメータ
    • なし

  • レスポンスパラメータ
    alg トークンの暗号化に使用される暗号化アルゴリズム
    e RSA公開鍵の指数値
    kid 開発者アカウントから取得した10文字の識別子キー
    kty キータイプのパラメータ設定。RSA
    n RSA公開鍵のモジュラス値
    use 公開鍵の使用目的

レスポンスは上記パラメータが複数返却されます。
いずれかがid_tokenを複合する際の正しい公開鍵の元情報になります。

# clinet secretの生成

client secretを生成する処理をPHPで書くとこんな感じになるかなと思います。
composer require firebase/php-jwt でFirebaseのJWTライブラリを導入しておいてください。

  use Firebase\JWT\JWT;

  $now = time();
  $expire = $now + 86400; // 有効期限1日

  $payload = [
    'iss' => temID,
    'iat' => $now,
    'exp' => $expire,
    'aud' => 'https://appleid.apple.com',
    'sub' => clientId,
  ];

  return JWT::encode($payload, secretKey, 'ES256', keyId);

# 公開鍵の生成

公開鍵の生成する処理をPHPで書くとこんな感じになるかなと思います。
composer require phpseclib/phpseclib:~3.0 でRSAのライブラリを導入しておいてください。

  use Firebase\JWT\JWT;
  use phpseclib3\Crypt\RSA;
  use phpseclib3\Math\BigInteger;
  
  $rsa = new RSA();

  $rsa->loadKey(
    [
      'e' => new BigInteger(JWT::urlsafeB64Decode(認証キー取得APIのレスポンス : e), 256),
      'n' => new BigInteger(JWT::urlsafeB64Decode(認証キー取得APIのレスポンス : n), 256),
    ]
  );
  $rsa->setPublicKey();

  return $rsa->getPublicKey();

#id_tokenのデコード

id_tokenをデコードする処理をPHPで書くとこんな感じになるかなと思います。

  use Firebase\JWT\JWT;
  use Firebase\JWT\Key;
  
  JWT::$leeway = 5; // 必要に応じて。署名サーバと検証サーバの時間のズレをn秒許容するらしい。エラーが出る場合は対応する。
  $decode = JWT::decode($idToken, new Key($publicKey, 'RS256'));

注目の投稿

composer.lockはGit管理すべき話

# 背景 # 結論 # composer installとcomposer updateの違い # 背景 仕事では主にLaravelを使用して開発を行っている。 ソースコードはGitでバージョン管理を行い、compose...

プロフィール

7年ほど中小企業でSIerとして働いていました。 現在は個人事業主としてPHP, Javascriptの企業案件をメインに受けています。 最近はポケモンカードの開封にはまっています。

アーカイブ

このブログを検索

QooQ