3
0

OpenID Connectの実装について

Posted at

はじめに

この記事ではOpenID Connect認証の流れと、PHPを用いたアクセストークンおよびIDトークンの取得、公開鍵の検証方法について説明します。
OpenID Connectは、OAuth 2.0をベースにした認証プロトコルであり、
ユーザーが認証情報を直接提供することなく、第三者アプリケーションに安全にアクセス権を委任できる仕組みです。


OpenID Connect基本的な処理の流れ

※Service Aは開発中のサイト
※Identity Providerは認証を行うサービス(例:Google、Facebookなど)

認証と認可の違いについて

認証とは、ユーザーが誰であるかを確認するプロセスです。
例えば、ユーザーがユーザー名とパスワードを入力して自分自身を証明する行為が認証に当たります。
認証が成功すると、ユーザーはシステムに対して「私はこの人です」と証明されたことになります。

一方、認可はそのユーザーがどのリソースにアクセスできるかを決定するプロセスです。
認証が完了した後に、ユーザーが何をできるか
(例えば、特定のデータを読み取る、書き込む、削除するなど)を管理するのが認可です。

例えば、OpenID Connectを使用すると、ユーザーは自分のGoogleアカウントを使って他のアプリケーションにログインできますが、そのアプリケーションにはユーザーのメールアドレスやプロフィール情報へのアクセス権限しか与えられない場合があります。これは、ユーザーがGoogleで認証され、その後アプリケーションが特定のリソースに対するアクセスを認可されている例です。


ログインリクエスト

開発中のサイトでログインボタンをクリックします。
サイトはユーザーをIdentity Providerにリダイレクトします。
ユーザーは認証情報を入力してログインします(シーケンス図の「User->>Identity Provider: 認証情報を入力してログイン」の部分)。
認証が成功すると、Identity Providerは認可コードを返します。

    {
        "code": "認可コード",
        // その他
    }

アクセストークンの取得

ユーザーが認可コードを取得した後、開発中のサイト(Service A)はこの認可コードを使ってトークンエンドポイントにリクエストを送ります。
このリクエストには、認可コードと共にクライアントIDやクライアントシークレットも含まれます。

リクエスト例

以下のような形でトークンエンドポイントにリクエストを送信します。

  • トークンエンドポイントにリクエスト(codeを添える)
    • grant_type=authorization_code&code=認可コード&redirect_uri=https://client.example.com/cb&client_id=クライアントID&client_secret=クライアントシークレット
    • レスポンスの解析
{
    {
    "access_token": "eyxxxxx.eyxxxxx.xxxxx",      // アクセストークン(JWT形式)
    "refresh_token": "リフレッシュトークン",                // リフレッシュトークン
    "scope": "openid profile email",              // トークンのスコープ(OpenID Connectを含む)
    "id_token": "eyxxxxx.xxxxx.yyyyy",            // IDトークン(JWT形式)
    "token_type": "Bearer",                       // トークンタイプ(通常は "Bearer")
    "expires_in": 3600                            // アクセストークンの有効期限(秒単位)
        }
}

このレスポンスには、アクセス トークンをデコードするとユーザー情報が含まれていることが確認できます。例えば、以下のような情報が含まれています。

{
    "alg": "RS256",
    "typ": "JWT"
}
.
{
    "sub": "1234567890",
    "name": "John Doe",
    "email": "john.doe@example.com",
    "iat": 1516239022,
    "exp": 1516242622
}
.
<SIGNATURE>

またOpenID Connectを使用している証拠としてid_tokenが含まれています。id_tokenは、ユーザーの認証情報を含むJWT形式のトークンであり、ユーザーのIDやプロフィール情報を確認するために使用されます。
OAuthだとid_tokenがレスポンスには含まれてないということです。


アクセストークンの検証

  1. 署名キーの取得

    {
        "kid": "X5eXk4xyojNFum1kl2Ytv8dlNP4-c57dO6QGTVBwaNk",
        "nbf": 1493763266,
        "use": "sig",
        "e": "AQAB",
        "n": "tVKUtcx_n9rt5afY_2WFNvU6PlFMggCatsZ3l4RjKxH0jgdLq6CScb0P3ZGXYbPzXvmmLiWZizpb-h0qup5jznOvOr-Dhw9908584BSgC83YacjWNqEK3urxhyE2jWjwRm2N95WGgb5mzE5XmZIvkvyXnn7X8dvgFPF5QwIngGsDG8LyHuJWlaDhr_EPLMW4wHvH0zZCuRMARIJmmqiMy3VD4ftq4nS5s8vJL0pVSrkuNojtokp84AtkADCDU_BUhrc2sIgfnvZ03koCQRoZmWiHu86SuJZYkDFstVTVSR0hiXudFlfQ2rOhPlpObmku68lXw-7V-P7jwrQRFfQVXw"
    }
    
  2. 公開鍵の生成

    • JWKから公開鍵を生成する方法
    • 公開鍵の使用例と説明
    /**
      * JWK (JSON Web Key) から RSA 公開鍵を生成するメソッド
      * 
      * このメソッドは、与えられたJWKから公開鍵を生成します。JWKにはモジュラス ('n') と公開指数 ('e') が含まれており、
      * それらを用いてRSA形式の公開鍵を生成します。
      * 
      * @param array $jwk_key JWK配列。公開鍵の 'n' と 'e' の値を含む。
      * @return \phpseclib3\Crypt\RSA\PublicKey 生成されたRSA公開鍵オブジェクト
      */
    public function createPublicKey($jwk_key)
    {
        $n = new BigInteger($this->urlSafeB64Decode($jwk_key['n']), 256);
        $e = new BigInteger($this->urlSafeB64Decode($jwk_key['e']), 256);
        
        $publicKey = PublicKeyLoader::load(['n' => $n, 'e' => $e]);
        return $publicKey;
    }
    
    
    
  3. 署名の検証

    • 署名の検証方法とサンプルコード
    • エラーハンドリングとデバッグ方法
    
    /**
     * URLセーフなBase64デコードを行うメソッド
     * 
     * このメソッドは、URLセーフなBase64エンコード形式の文字列をデコードします。
     * Base64エンコード形式は、通常のBase64とは異なり、URLで使用できる形式に変換されています。
     * 
     * @param string $input URLセーフなBase64エンコード文字列
     * @return string デコードされたバイト文字列
     */
    public function urlSafeB64Decode($input)
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $padlen = 4 - $remainder;
            $input .= str_repeat('=', $padlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }
    
    /**
     * JWTの署名を検証するメソッド
     * 
     * このメソッドは、JWT (JSON Web Token) のヘッダーとペイロードを元に、与えられた署名が正当かどうかを検証します。
     * 署名の検証には、JWKから生成されたRSA公開鍵を使用します。正当な署名であればtrueを返し、無効またはエラーが発生した場合はfalseを返します。
     * 
     * @param string $header_encoded JWTのヘッダー部分(Base64URLエンコード済み)
     * @param string $payload_encoded JWTのペイロード部分(Base64URLエンコード済み)
     * @param string $signature_encoded JWTの署名部分(Base64URLエンコード済み)
     * @param array $jwk_key JWK配列。公開鍵の 'n' と 'e' の値を含む。
     * @return bool 署名が正当であればtrue、無効またはエラーが発生した場合はfalseを返却
     */
    public function verifySignature($header_encoded, $payload_encoded, $signature_encoded, $jwk_key)
    {
        $publicKey = $this->createPublicKey($jwk_key);
        $signature = $this->urlSafeB64Decode($signature_encoded);
        $data = $header_encoded . '.' . $payload_encoded;
        
        // JWTの署名を検証
        $result = openssl_verify($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);
        
        if ($result === 1) {
            return true; // 署名が正当
        } elseif ($result === 0) {
            Log::error("Signature verification failed."); // 署名が無効
            return false;
        } else {
            while ($msg = openssl_error_string()) {
                Log::error("OpenSSL Error: " . $msg); // OpenSSLのエラー
            }
            return false; // エラーが発生
        }
    }
    
    

公開鍵エラーの調査内容

実装中に発生したエラー内容が以下になります。

OpenSSL Error: error:0909006C:PEM routines:get_name:no start line  
OpenSSL Error: error:04091077:rsa routines:int_rsa_verify:wrong signature length

openssl-verify関数について
https://www.php.net/manual/ja/function.openssl-verify.php

id_tokenの精査

公開鍵の精査

改行追加
https://qiita.com/ryou6152/items/9639db3e6836fd96239a
文字列にする
https://stackoverflow.com/questions/74842756/php-laravel-openssl-error0909006cpem-routinesget-nameno-start-line

$publicKeyPem = "-----BEGIN PUBLIC KEY-----\n".
        "xxxxxxxxxxxxxxxxxxxxxxxxxxx\n".
        "xxxxxxxxxx\n".
        "xxxxxxx\n".
        "xxxxxxx\n".
        "xxxxxxx\n".
        "xxxxxxxx\n".
        "xxxxxxxx\n".
        "-----END PUBLIC KEY-----\n";

エラー内容が変わらず解決まで至らなかったので
結局以下のライブラリを使用し公開鍵の検証まで実装できました。
spomky-labs

    // JWKオブジェクトを作成(公開鍵を作成)
    $jwk = new JWK($jwk_key);

    // JWKSetオブジェクトを作成
    $jwkSet = new JWKSet([$jwk]);

    // アルゴリズムマネージャを作成(RS256を使う)
    $algorithmManager = new AlgorithmManager([
        new RS256(),
    ]);

    // JWSVerifierを作成(JWTの署名が有効かどうかを検証)
    $jwsVerifier = new JWSVerifier($algorithmManager);

    // シリアライザを作成(JWTを文字列からオブジェクトに変換)
    $serializer = new CompactSerializer();

    // トークンをロードして検証する(デシリアライズしてJWSオブジェクトに変換)
    $token = $oauth_tokens['id_token'];
    $jws = $serializer->unserialize($token);

    // 署名を検証無効の場合エラー
    if (!$jwsVerifier->verifyWithKeySet($jws, $jwkSet, 0)) {
        Log::error('公開鍵の検証に失敗しました。');
        return \App::abort(500);
    }

まとめ

OpenID Connectを利用した認証プロセスについて学ぶことで、他のサービスを利用した認証の仕組み、
トークンにはユーザー情報が含まれており、その情報がどのように保護されているかを実際に確認することで、
理解を深めることができました。

参考資料

3
0
0

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
3
0