2017.01.04

OpenID Connect IDトークン署名検証

こんにちは。次世代システム研究室のM.Mです。
過去のブログにてOpenAMを利用したOpenID Connectについて紹介したことがありましたが、その際はOpenID ConnectのIDトークンの検証については省略していたので、今回はその続きとしてIDトークンの署名検証についてPHPにてどのように実施したか紹介したいと思います。

過去のブログはこちら
OpenID Connectについてはこちら

・OpenID Connectプロバイダーとしては前回同様OpenAMを使って確認を行っています。
・このブログに記載しているIDトークンの値などは実際の値を別の値に加工しているので利用することはできません。

目次

  1. IDトークンとは
  2. IDトークン構造について
  3. PHPによるIDトークンの検証

1.IDトークンとは

ユーザーがOpenID Connectプロバイダーにてログイン認証した後、OpenID ConnectプロバイダーからOpenID Connect利用サービスに渡される認証結果が格納されたトークンになります。
そのトークンには発行者、Subject識別子、認証の有効期限などが含まれており、OpenID Connect利用サービスは発行者が正しいかなどトークンを適切に検証する必要があります。

2.IDトークン構造について

IDトークンの構造はJWSとなっており電子署名が付いているトークンとなっています。
JWSの構造はヘッダー情報、ペイロード情報、署名を.(ドット)で区切った値になっており、各値はBASE64URLエンコードされています。
JWSの資料には以下のように記載されています。

BASE64URL(UTF8(JWS Protected Header)) || ‘.’ ||
BASE64URL(JWS Payload) || ‘.’ ||
BASE64URL(JWS Signature)

 

IDトークンの値サンプル(表示上改行を入れています)

eyAidHlwIjogIkpXVCIsICJraWQiOiAia0FYSVMxbWlZaGJBU2
cwUHpkSjFKVzRaQ3FBPSIsICJhbGciOiAiUlMyNTYiIH0.eyAi
c3ViIjogIjEyMzQ1NiIsICJpc3MiOiAiaHR0cDovL29wZW5hbS
5leGFtcGxlLmNvbTo4MDgwL29wZW5hbS9vYXV0aDIiLCAidG9r
ZW5OYW1lIjogImlkX3Rva2VuIiwgIm5vbmNlIjogIjEyMzQ1Ii
wgImF1ZCI6IFsgInRlc3RzZXJ2aWNlMSIgXSwgIm9yZy5mb3Jn
ZXJvY2sub3BlbmlkY29ubmVjdC5vcHMiOiAieHh4eHh4eHgteH
h4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwgImF6cCI6ICJ0
ZXN0c2VydmljZTEiLCAiYXV0aF90aW1lIjogMTQ4MjIxODI0MS
wgInJlYWxtIjogIi8iLCAiZXhwIjogMTQ4MjIyODgyNCwgInRv
a2VuVHlwZSI6ICJKV1RUb2tlbiIsICJpYXQiOiAxNDgyMjI1Mj
I0IH0.XJfOH7jKphBO0UoSxmdCxYx1fWKlAjPyo1VfJbf5KSYp
rPPgHIgCpug6TJjg3LgsSiyZV_AXg2U11Q6hsJdvkOcrFLAaMz
Dm0uvWfZRVewmbg_Oy6mFDlBvwoykAz8ip9hqHA4frTQc71o5J
sGieLCAE0Xu-n6gzJayvrLqoXSkEPGBs_vl4bQh7x1DI2xZu3e
HpFKcM8EiBeTq3hcSMJTBsO23p993CE4likuHhOabYcAQeeC6m
2ZZB7FCCL1Xa8kYL1_z3Uw_xvKXsLCe10cQc8WfZ3X2lRDLDaG
PjwPq6YxD33ehWTcDG6uftb7L42j5TJJ3Nmh1ZaNK3gfeq7w
2行目と12行目にある.(ドット)で区切られています。

それぞれをBASE64URLデコードすると以下のような内容が取得できます。

■ヘッダー部分

{ 
  "typ": "JWT",
  "kid": "kAXIS1miYhbASg0PzdJ1JW4ZCqA=",
  "alg": "RS256"
}

■ペイロード部分

{
  "sub": "123456",
  "iss": "http://openam.example.com:8080/openam/oauth2",
  "tokenName": "id_token",
  "nonce": "12345",
  "aud": [ "testservice1" ],
  "org.forgerock.openidconnect.ops": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "azp": "testservice1",
  "auth_time": 1482218241,
  "realm": "/",
  "exp": 1482228824,
  "tokenType": "JWTToken",
  "iat": 1482225224
}

■署名部分

BASE64デコードするとバイナリデータとなります。

3.PHPによるIDトークンの検証

上記IDトークンをBASE64URLデコードして得られる情報を利用して正当性のチェックを行うことになりますが、検証内容はOpenID Connectとの連携フロー(Authorization Code FlowやImplicit Flowなど)によって必須となる検証項目が異なっていたり、また多くの検証項目が存在します。
今回は一番面倒だった署名検証について紹介します。

Authorization Code Flowのステップに沿って確認してきます。
Authorization Code Flowは以下となっています。(OpenID Connect資料より)

The Authorization Code Flow goes through the following steps.

1.Client prepares an Authentication Request containing the desired request parameters.
2.Client sends the request to the Authorization Server.
3.Authorization Server Authenticates the End-User.
4.Authorization Server obtains End-User Consent/Authorization.
5.Authorization Server sends the End-User back to the Client with an Authorization Code.
6.Client requests a response using the Authorization Code at the Token Endpoint.
7.Client receives a response that contains an ID Token and Access Token in the response body.
8.Client validates the ID token and retrieves the End-User’s Subject Identifier.

 

フローNo1,No2のPHPサンプル

1.Client prepares an Authentication Request containing the desired request parameters.
2.Client sends the request to the Authorization Server.
// 1.Client prepares an Authentication Request containing the desired request parameters.
$params = '?response_type=code'.
          '&client_id=testservice1'.
          '&redirect_uri='.urlencode('http://testservice1.example.com/cb').
          '&scope='.urlencode('openid email').
          '&display=page'.
          '&nonce=abcdef'.
          '&state=123456'

// 2.Client sends the request to the Authorization Server.
$endopoint = 'http://openam.example.com:8080/openam/oauth2/authorize'
header('Location: '.$endpoint.$params);
 

フローNo3,No4,No5

3.Authorization Server Authenticates the End-User.
4.Authorization Server obtains End-User Consent/Authorization.
5.Authorization Server sends the End-User back to the Client with an Authorization Code.

OpenID Connectプロバイダー側の処理なのでここでは省略します。
ユーザーのログイン、同意などを得て、このフローが完了すると認可コードを付けて指定したURLに戻ってきます。(上記フローNo1,No2のPHPサンプルでは4行目redirect_uriにhttp://testservice1.example.com/cbを指定しているので、そのURLにcodeパラメータが付いてリダイレクトされます。)
 

フローNo6のPHPサンプル

6.Client requests a response using the Authorization Code at the Token Endpoint.
以下はcodeパラメータが付いてリダイレクトされてきたページの処理になります。
codeパラメータを利用してToken Endpointにリクエストしトークン情報を取得しています。
$code = isset($_GET['code']) ? $_GET['code'] : '';
if ($code != '') {
   $ch = curl_init();
   curl_setopt($ch, CURLOPT_URL,
  'http://openam.example.com:8080/openam/oauth2/access_token'
      .'?grant_type=authorization_code'
      .'&redirect_uri='.urlencode('http://testservice1.example.com/cb')
      .'&code='.$code);
   curl_setopt($ch, CURLOPT_USERPWD, 'testservice1:************');
   curl_setopt($ch, CURLOPT_POST, true);
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
   $json = curl_exec($ch);
   curl_close($ch);
 
   $token_info = json_decode($json);
}
 

フローNo7

7.Client receives a response that contains an ID Token and Access Token in the response body.
フローNo6で取得したデータは以下の通り(表示上改行を入れています)
{
  "access_token":"02e2b2ef-01ff-4000-9433-00834f9ae6c2",
  "refresh_token":"f8e7f144-04e2-46d2-8a10-451ac7f87d64",
  "scope":"openid profile",
  "id_token":
"eyAidHlwIjogIkpXVCIsICJraWQiOiAia0FYSVMxbWlZaGJBU2
cwUHpkSjFKVzRaQ3FBPSIsICJhbGciOiAiUlMyNTYiIH0.eyAi
c3ViIjogIjEyMzQ1NiIsICJpc3MiOiAiaHR0cDovL29wZW5hbS
5leGFtcGxlLmNvbTo4MDgwL29wZW5hbS9vYXV0aDIiLCAidG9r
ZW5OYW1lIjogImlkX3Rva2VuIiwgIm5vbmNlIjogIjEyMzQ1Ii
wgImF1ZCI6IFsgInRlc3RzZXJ2aWNlMSIgXSwgIm9yZy5mb3Jn
ZXJvY2sub3BlbmlkY29ubmVjdC5vcHMiOiAieHh4eHh4eHgteH
h4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwgImF6cCI6ICJ0
ZXN0c2VydmljZTEiLCAiYXV0aF90aW1lIjogMTQ4MjIxODI0MS
wgInJlYWxtIjogIi8iLCAiZXhwIjogMTQ4MjIyODgyNCwgInRv
a2VuVHlwZSI6ICJKV1RUb2tlbiIsICJpYXQiOiAxNDgyMjI1Mj
I0IH0.XJfOH7jKphBO0UoSxmdCxYx1fWKlAjPyo1VfJbf5KSYp
rPPgHIgCpug6TJjg3LgsSiyZV_AXg2U11Q6hsJdvkOcrFLAaMz
Dm0uvWfZRVewmbg_Oy6mFDlBvwoykAz8ip9hqHA4frTQc71o5J
sGieLCAE0Xu-n6gzJayvrLqoXSkEPGBs_vl4bQh7x1DI2xZu3e
HpFKcM8EiBeTq3hcSMJTBsO23p993CE4likuHhOabYcAQeeC6m
2ZZB7FCCL1Xa8kYL1_z3Uw_xvKXsLCe10cQc8WfZ3X2lRDLDaG
PjwPq6YxD33ehWTcDG6uftb7L42j5TJJ3Nmh1ZaNK3gfeq7w",
  "token_type":"Bearer",
  "expires_in":3599,
  "nonce":"12345"
}
 

フローNo8(ここがIDトークンの検証箇所になります)

8.Client validates the ID token and retrieves the End-User’s Subject Identifier.

Authorization Code Flowの署名検証部分についてOpenID Connectの資料には以下のように記載されています。

If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS] using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer.

サーバー間通信をするAuthorization Code Flowの場合は、署名検証の代わりにTLS Serverの検証でよいみたいなことが記載されていますが、今回は署名検証を実施します。(署名検証はImplicit Flowの場合は必須のようです。)
また上記説明から、署名検証のためのアルゴリズムはHeader Parameterにある、署名検証には発行者から提供されたキーを使う必要があると分かります。

署名検証のためのアルゴリズムは何か?
上記IDトークン構成で説明したヘッダー部分を見ると以下のような情報が取得できています。

{ 
  "typ": "JWT",
  "kid": "kAXIS1miYhbASg0PzdJ1JW4ZCqA=",
  "alg": "RS256"
}
その中のalgというパラメータを見ればアルゴリズムはRS256だと分かります。

署名検証に使うキーはどのように取得するのか?
OpenAMでは
http://openam.example.com:8080/openam/oauth2/connect/jwk_uri
にアクセスすると以下のようなキー情報がJSONで取得できます。(表示上改行を入れています)
{
"keys":[{
  "kty":"RSA",
  "kid":"kAXIS1miYhbASg0PzdJ1JW4ZCqA=",
  "use":"sig",
  "alg":"RS256",
  "n":
"AJYHdgqJUxUM-1TXlwbPoT8wu1SOIgxlRZCMTDLWFLlLXS5HDv
ePktJrTGT3nEIA8e3a2pYzzS-pwtnory5ZVHIiPOlFPaHwYMEJ
rfwtw3fEhZZ4v6Ktckqanw7vW0SFo4mWtY5A_2aduAQOhEskgC
-BuPaTw9p8G3IGEMSQHWHqU9qg3pYpALjWbaiFSh1qFmFq2UDA
f19bXf1Ps7P0UONg6OMZkq9mOqqeemJ1FR8TO8s_-R4Z-7Q8L6
X-EkrI-FnmPy5ZxslgfLVz_cRZk6gSGpd7EI0FAlK3jbgX80fU
TpvVpwuX_MyRMCrLN00GejjCt0Mwk26mW48klVPY3GU",
  "e":"AQAB"
  }]
}
keys配列の中からヘッダー部分に存在したkidの値にマッチするnパラメータとeパラメータを利用して公開鍵を生成する必要があると分かります。

上記を踏まえ署名検証したPHPサンプル

RSA、JWSを扱うライブラリとして以下を利用しています。
Lcobucci
phpseclib
            // nパラメータ、eパラメータを利用して公開鍵を生成する
            // 実際にはhttp://openam.example.com:8080/openam/oauth2/connect/jwk_uriにアクセスし
            // ヘッダーから取得したkidにマッチした値を取得します
            $e = 'AQAB';
            $n = 'AJYHdgqJUxUM-1TXlwbPoT8wu1SOIgxlRZCMTDLWFLlLXS5HDv'.
                 'ePktJrTGT3nEIA8e3a2pYzzS-pwtnory5ZVHIiPOlFPaHwYMEJ'.
                 'rfwtw3fEhZZ4v6Ktckqanw7vW0SFo4mWtY5A_2aduAQOhEskgC'.
                 '-BuPaTw9p8G3IGEMSQHWHqU9qg3pYpALjWbaiFSh1qFmFq2UDA'.
                 'f19bXf1Ps7P0UONg6OMZkq9mOqqeemJ1FR8TO8s_-R4Z-7Q8L6'.
                 'X-EkrI-FnmPy5ZxslgfLVz_cRZk6gSGpd7EI0FAlK3jbgX80fU'.
                 'TpvVpwuX_MyRMCrLN00GejjCt0Mwk26mW48klVPY3GU';
            $pre_arr = array('_', '-', '');
            $post_arr = array('/', '+', '=');
            $dec_n = str_replace($pre_arr, $post_arr, $n);

            $rsa = new RSA();
            $rsa->loadKey(
                array(
                    'e' => new BigInteger(base64_decode($e), 256),
                    'n' => new BigInteger(base64_decode($dec_n), 256)
                )
            );
            // 公開鍵は$rsa->getPublicKey()で取得できる

            // 検証処理実施 
            $token = (new Parser())->parse((string) $token_info->{'id_token'});
            $verify_result = $token->verify(new Sha256(), $rsa->getPublicKey());

            // $verify_resultがtrueであれば検証成功
            // $token_info->{'access_token'}を利用してuserinfoにて情報を取得する
 

所感

上記12-14行目にある変換処理を入れる必要があることに気づかず数時間も悩んでしまいました。
OpenID ConnectについてはGoogleなどで調べるといろいろ資料がでてきます。
もちろん参考にさせていただいていますが、やはり最終的には正式なドキュメントを読んで理解することが求められると感じました。
まだまだ分からないところも多いですが不明点はしっかりつぶしていこうと思います。

最後に

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事