OpenAMのインストール

＜＜過去のブログを自分で読む用のエントリです。＞＞

IdP･･･ログインシステムを提供するカッチョイー人のこと。
RP･･･自前でログインシステムを構築することをあきらめた賢明な愚民。

今回は手元にあるレンタルサーバ群でこいつらを構築することにする。本来であればIdP側はgoogleとかfacebookとかtwitterとかを使うと思っていただければ問題あるまい。今回はまぁ、タイトルどおりIdP側を構築してみる、というのが主旨なので。

フェデレーションプロトコルといえば、”SAML”か”OpenID”なのだが、次世代プロトコルであるOpenID ConnectはOAuth2.0のシーケンスで認証連携ができるというものだ。先に中身が知りたい人はどうぞこちらのリンク(OpenIDファウンデーション・ジャパン技術使用：和訳)へ。とっても読みやすい。
構築眺めてなんとなく雰囲気がわかればいいや、という人はどうぞこのまま読み進めてくれ。

・OpenAMの導入

早速導入である。以下は参考にしたURL。
OpenAM インストール

事前にtomcatなどのWebサーバやJavaのインストールが必須となる。割愛。
OpenAMのインストール自体はどえらく簡単で、warファイルをダウンロードしてきてデプロイするだけだ。
今回は/opt配下にあらかじめopenamというディレクトリを作って
#mkdir /opt/openam
tomcat資材をブチこんだ。

あとは、最新のOpenAM資材をforgerock.orgから入手する。
#cp -p cp -p OpenAM-12.0.0-SNAPSHOT_20140315.war /opt/openam/apache-tomcat-6.0.39/webapps/
これだけ。

openamの起動にはいくつか指定があるので、openamの起動ファイルはリンクを参考にきちんと作ろう。まるぱくりではあるが、こんな感じ。
#vi /etc/init.d/openam
#!/bin/sh
#
# openam
#
# chkconfig: 35 84 16
# description: Manage OpenAM SSO Server
CATALINA_HOME=/opt/openam/apache-tomcat-6.0.39
export CATALINA_HOME
 
JAVA_HOME=/usr/java/default/
export JAVA_HOME
 
JAVA_OPTS="-Xmx1024m -XX:MaxPermSize=256m"
export JAVA_OPTS
 
CATALINA_OPTS="-Dcom.iplanet.am.cookie.c66Encode=true"
export CATALINA_OPTS
 
case "${1}" in
start)
"${CATALINA_HOME}/bin/startup.sh"
exit ${?}
;;
stop)
"${CATALINA_HOME}/bin/shutdown.sh"
exit ${?}
;;
*)
echo "Usage: $0 { start | stop }"
exit 1
;;
esac
view rawopenam hosted with ❤ by GitHub
必要なのは以下の２つの環境変数なので、忘れずに。
JAVA_OPTS=”-Xmx1024m -XX:MaxPermSize=256m”
export JAVA_OPTS

CATALINA_OPTS=”-Dcom.iplanet.am.cookie.c66Encode=true”
export CATALINA_OPTS

もう一点、openamの動的設定にあたりhostname設定はきちんとやっておく必要がある。
# hostname test.kirimanjirou.com
今回は上記のドメイン(無登録なので動かないと思うよすまぬ)でトライ。一応連携するサーバや手元のWindowsPCのhostsファイルにも書いておこう。ちなみにレンタルサーバだとデフォルトで適当なhostnameがふられているから、後で困る。

ここまで出来たら起動である。
# /etc/init.d/openam start

Using CATALINA_BASE: /opt/openam/apache-tomcat-6.0.39
Using CATALINA_HOME: /opt/openam/apache-tomcat-6.0.39
Using CATALINA_TMPDIR: /opt/openam/apache-tomcat-6.0.39/temp
Using JRE_HOME: /usr/java/default/
Using CLASSPATH: /opt/openam/apache-tomcat-6.0.39/bin/bootstrap.jar

カブあがれ～(パクリ)

tomcatの接続ポートを変更したり、apacheから流すなどの設定は今回はパスする。早速openamに接続してみよう。
http://test.kirimanjirou.com:8080/openam/

このとき、仮にローカルPCに構築していた場合でもhttp://127.0.0.1/openam、と接続しないように。初期設定を行うアクセスURLで認証連携時のフォワードURLが決定してしまうようだ。変えられるのかどうか分からないので初めから利用予定のドメインにしよう。直IPでアクセスするのも推奨されない。

Idp_1.fw
うむ、なにやらシャレオツなページが出たようだ。右側の設定ボタンで進んでいこう。

Idp_2.fw Idp_3.fwIdp_4.fwIdp_5.fwIdp_6.fwIdp_7.fwIdp_8.fw

最後までいったら設定ボタンを押そう。最近はこうやってWebコンソールからなんでも初期設定ができるの、便利だよなあ。

Idp_9.fw

Idp_10.fw

上に書いたみたいにhostnameが設定されていなかったり、していると失敗のメッセージが出る。(サフィックスエラー１ connection refusedが出てしまったときはテストドメインへの接続に失敗している)
なお、最初からやり直したい場合は中途半端に出来上がったopenamのファイルを消そう。(まだディレクトリが出来ていない場合は省略してよい)
#rm -rf /root/openam
#rm -rf /opt/openam/apache-tomcat-6.0.39/webapps/openam

そつなくこなせていれば、ログインページに進めるはずである。

IdP_11.fw

ちなみにデフォルトユーザはamadminだ。初期設定以降書いてねえから気をつけろ。

・OpenAMの設定
ログインまではこの分野の知識ゼロでもかなり楽チンだが、ここから先は少し難しい。ログイン直後から小難しいメニュー群の行列である。

Idp_12.fw
えーなんだ、えーえー、なんだ。OpenIDConnectはどこだ。
まずはOpenID ConnectをはじめるためにOAuth2.0サーバとしての初期設定がいるようだ。画面中央のOAuth2の設定を押す。

Idp13.fw

適当にトークンの有効期限を延ばして、右上の作成をPush。

・RPの登録

Idp14.fw

「アクセス制御」→(最上位のレルムを選択)→「サービス」→「OAuth2.0クライアント」→上側の段で「新規…」

Idp_16.fwIdp_17.fw
Idp_18.fwIdp_19.fw

最低限必要な情報を登録しておく。名称、説明、スコープあたり。

まだクライアントがないのでリダイレクトURIは迷うところだが、とりあえずあとで使う予定の”http://kirimanjirou.com/sample/signup.php”こんなんでいいや。ちなみに、複数登録できる。
今回はユーザ情報を取りたいのでいくつかprofile関連のスコープを入れる。
あとID Token Signed Response Algorithm:の項目がカラッケツだと後述のID Tokenが動かない。ここではHS256を入れておく。
保存を押せばクライアントの登録が完了だ！

なお、OpenAMサポートしているクライアント登録の方法はいくつかあるようだ。

・Webコンソールからグイグイ掘っていく。

「アクセス制御」→(最上位のレルムを選択)→「エージェント」→「OAuth2.0クライアント」からエージェントを新規登録
まぁこれが一番無難で簡単だ。

・用意された謎の登録画面を押す
http://test.kirimanjirou.com:8080/openam/oauth2/registerClient.jsp

IdP_21.fw

まぁこれを拡張しなさいよというサンプルコンテンツだろうな。

・Registrationエンドポイントを使う

ここまでは、今までのOAuthのクライアント登録手続きと一緒だろう。
client_idとclient_secretだけの状態から登録する手続きを実装した模様なので、これを使う方法が実装されている。
どうやらOpenID Connectで使うDynamic Client Registrationというやつだなこれは。

ややこしいのだが、以下の順番で登録ことが出来るようだ。
1.client_idとclient_secretをamadin権限でアクセストークンエンドポイントに入力する。
2.Registration操作用のアクセストークンが出てくるので、これを使って上記のregistrationエンドポイントにアプリ名や利用スコープ、もろもろの情報を登録する。
ポイントはtwitterやfacebookアプリみたいな画面手続きじゃなくてもクライアントの発行手続きが出来ることか。ちなみに詳細はここに記述されている。

・ユーザの作成

OpenAMの導入時にもともとシステムで入れてたユーザ管理(LDAPとかActiveDirectoryとか)と連動させるオプションがあるが、今回は動かしてない。その代わりログインテストに使うユーザ”maniopenidconnect”を作成している。

Idp_20.fw

「アクセス制御」→(最上位のレルムを選択)→「対象」→「新規…」

IdP_22.fw

 

･open connect providerとしての動作確認

で！！ここまで用意できればとりあえずIdP側のセットアップは出来たということになる。早速手打ちで認可の画面を出してみよう。
現状はamadminでログインしてしまっていると思うので、ログアウトしてテストユーザのmanjiopenidconnectでログインするよ。
クライアントは登録したkirimanjirou003を使う。

http://test.kirimanjirou.com:8080/openam/oauth2/authorize?client_id=kirimanjirou003&response_type=token&scope=phone address openid profile

Idp_15.fw

ショッボッー！！！だが動いている、せっかくなのでもう少し動かしてみよう。
(ちなみにリダイレクト先アプリはないから、以下のパラメタはブラウザのエラー画面から拾っている。お許しあれ)

<Request:適当な環境から>
curl -H  “Authorization:  Bearer 2b3494ef-fc56-4ad4-ba9d-93ee9608a459” http://test.kirimanjirou.com:8080/openam/oauth2/userinfo

<Response>
{
“phone”:”090********”,
“sub”:”manjiopenidconnect”,
“email”:”kirimanjirou@gmail.com”,
“address”:”Ota ward,Tokyo”,
”name”:”霧卍朗”,
“family_name”:”霧”,
“given_name”:”卍朗”
}

ああ、そうねそうね。あれでも肝心のopenidがなくねーかこれ。
ちなみに、きちんとcode flowにのっとったID tokenの取得も出来るようだ。

では初物のID tokenはどうだろうか。
<Request>
http://test.kirimanjirou.com:8080/openam/oauth2/authorize?client_id=kirimanjirou003&response_type=id_token

<Responseに乗ってきたidtoken>
id_token=eyAiYWxnIjogIkhTMjU2IiwgImN0eSI6ICJKV1QiLCAidHlwIjogImp3dCIgfQ.eyAidG9rZW5OYW1lIjogImlkX3Rva2VuIiwgImV4cCI6IDEzOTU0NDkzNTAsICJhenAiOiAia2lyaW1hbmppcm91MDAxIiwgInN1YiI6ICJhbWFkbWluIiwgInRva2VuVHlwZSI6ICJKV1RUb2tlbiIsICJub25jZSI6ICI1WU9WNDRHbzVhV1I1N1NFNDRHWDQ0R21TVVRsanFqamdhdmpnYXJqZ2FQamdhYmpnb2c9IiwgInJlYWxtIjogIi8iLCAiYXVkIjogWyAia2lyaW1hbmppcm91MDAxIiBdLCAiaXNzIjogImh0dHA6Ly90ZXN0LmtpcmltYW5qaXJvdS5jb206ODA4MC9vcGVuYW0iLCAiaWF0IjogMTM5NTQ0ODc1MCwgIm9wcyI6ICJBUUlDNXdNMkxZNFNmY3k3dC1UOElMV3pNejJ1aTdTbGo3ckhuUkFtTGhIeHIwOC4qQUFKVFNRQUNNREVBQWxOTEFCTTVNVEV4TXpRMU5qWXpORFV3TmpJd01qZzEqIiwgImF0aCI6IDEzOTU0NDg3NTAgfQ.Ky0X5Y4AY4Nj0gHgN4vjR2YSfZ5o7jLfx_5dKHQOdK8

BASE64デコードする。手動で実施する際は、ピリオドを跨いで３つに分割されていることに注意しないとうまく分割できんぞ。

JWTヘッダー：
{
“alg”: “HS256”,
“cty”: “JWT”,
“typ”: “jwt”
}
中身：
{
“tokenName”: “id_token”,
“exp”: 1395449350,
“azp”: “kirimanjirou003”,
“sub”: “manjiopenidconnect”,
“tokenType”:”JWTToken”,
“nonce”:”5YOV44Go5aWR57SE44GX44GmSUTljqjjgavjgarjgaPjgabjgog=”,
“realm”: “/”,
“aud”: [ “kirimanjirou003” ],
“iss”: “http://test.kirimanjirou.com:8080/openam”,
“iat”: 1395448750,
“ops”: “AQIC5wM2LY4Sfcy7t-T8ILWzMz2ui7Slj7rHnRAmLhHxr08.*AAJTSQACMDEAAlNLABM5MTExMzQ1NjYzNDUwNjIwMjg1*”,
“ath”: 1395448750
}

署名(未デコード)：
Xrh5ev4qec1gDqpuY7qB35og6OLCjyjiCKfFYXQ177A
おー出てきた出てきた、id_tokenである。細かい項目内容の説明は省略。また、取れたものの正当性を検証する手続きもあるんだがこれは次回で。
うーん、ところで、opsって属性はなんだろう、これだけわからないな。

・生成されたエンドポイントと、プロバイダ情報エンドポイントから取れる情報

ここでいったん、OpenAMを設定したことで何ができるようになったのかを確認しよう。
というかこのチャプタはRP登録の前に説明したほうがよさそうだったんだがな、何でかしらんがOAuthのエンドポイントを一回たたかないとプロバイダ情報エンドポイントが動かなかったんだよ…。

説明をはしょってしまったが、上記の手順を踏むことで以下のエンドポイントを生成することができる。
・プロバイダ情報エンドポイント
http://test.kirimanjirou.com:8080/openam/.well-known/openid-configuration

OpenID Connectを提供するIdPの情報をここから取得することが出来る。

・認可エンドポイント　/oauth2/authorize
しょっぱなに認可リクエストを飛ばすポイント。OAuth2.0と同じだね。

・アクセストークンエンドポイント　/oauth2/access_token
トークンの取得、更新が出来てるのはOAuth2.0と同じだね。上述のとおり、クライアント登録用のトークンを取る方式とかもサポートしているのだね。

・ユーザ情報エンドポイント　/oauth2/userinfo
アクセストークンからユーザ情報を取得するためのエンドポイント。
id tokenに含まれない詳細情報を入れるってのが標準的な使い方なのか？ここはid tokenの中身と違いがわかんねーんだけど、って質問がこないだのtech nightでもあったよね。

・Registrationエンドポイント　/oauth2/connect/register
上述の、Dynamic Registrationのためのエンドポイントである。

・ログアウトエンドポイント　/oauth2/connect/endSession
まだ動かしてないのでノーコメント。

・セッション確認エンドポイント　/oauth2/connect/checkSession
同上。

以下はプロバイダ情報エンドポイントから読める設定内容。多少めんどうだがOpenID Connectの理解には重要な項目なんで、読んでいこう。

IdP_23.fw

■response_types_supported
id tokenの取得フロー、方式について何をサポートしているかどうかが記述される。

■version
3.0、ということでございやす。

■subject_types_supported
利用可能なクライアント種別。なぜかデフォルトはpublicのみのようだ。

■issuer
IdPを一意に表すURL。律儀に表示してくれたということで。

■jwks_uri
ここの設定が何を意味するのかどうかはまだわかっていない。

■check_session_iframe
ログイン状態を確認するエンドポイント。どんなもんが返ってくるのかしら。

■claims_supported
サポートするユーザ属性。こちらもOpenAMの設定から追加や削除ができるようだ。

■id_token_signing_alg_values_supported
サポートする署名アルゴリズムを提示。

■userinfo_endpoint
■registration_endpoint
■token_endpoint
■end_session_endpoint
■authorization_endpoint
上で紹介した各種エンドポイントのあて先を示す。

なお、これらの設定をいじくるためのGUIは以下からたどれる
「アクセス制御」→(最上位のレルムを選択)→「サービス」→「OAUTH2プロバイダ」
そのほか、OpenAMの公式ドキュメントはそれなりに読みやすいので、こちらもオススメである。ワシも絶賛精読中。

気になることはたくさんあるが、とりあえず連携はできそうなので、早速client側の実装にいってみよう。

前回のエントリでIdPの組み上げに成功した。では実際に組み上げたIdPと連携するログインシステムを作ってみよう。

今回は「いや、そもそもOAuthからしてよくわからねえんだよなてへぺろ」という方を対象に、OpenID Connect対応サイトを作ってとりあえず動かすところまでをフォローすることにする。もうそんなの知ってるﾜｲな人は今回のエントリは読み飛ばしてくれても構わないぞ。
なお、ワシの糞ースにつきあってられるか、という人はYConnectなどの整頓されたライブラリから実装を追うのがオススメだよ。今回はフローを一通り追うのが目的なので、ベタ書きしているよ。

実際に動いているサービスに組み込んでいく、とかはまた次回にトライである。
以下、本題。

作るものはいくつかあるが、今回用意するのは以下のコンテンツとなる。
1.ログイン画面
2.自作IdPにブッ飛ばす
3.自作IdPからブッ飛ばされてきて、トークンをとりにいく
4.ログインする
5.ログアウトする

1.ログイン画面
ログイン画面はなんでもいい。ここは好みで作るがよい。
とはいえ手抜きはせず、ワシのハイセンスなデザインでカカッと実装していく。
RP_2

index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Manji Sample</title>
</head>
<body>
<h1>自前IdPにログインする</h1>
<p>
<a href="login.php">
<img src="http://kirimanjirou.com/sample/login.png" width="241" height="28" alt="自作IdPにログ
イン！" border="0">
</a>
</body>
</html>
view rawindex.html hosted with ❤ by GitHub

2.自作IdPにブッ飛ばす
ここからが本丸である。 言い忘れたが今回は普通のWebログインシステムをサンプルで組むので、 OpenID Connectの”codeフロー”という奴を利用する。 ポイントはID Tokenの受け渡しにブラウザを経由させない、ということだ。 サイト側の手間は一個増えるが、ユーザのブラウザにID Tokenが残る心配がない。ブラウザを経由してくるのはID Tokenと一回ポッキリの引き換えが出来る”code”というものだ。 流れはこんな感じになる。
ハイセンスなログインボタンを押す
これからID Tokenを取りに行くぜ！という気合を自作IdPに届ける。リダイレクト。
ユーザがログインしていない場合、ログイン。こないだたてたOpenAMの画面だね。
ユーザが天↑TENGAアプリを許すかどうかのジャッジメント
許された場合はユーザがcodeをもって戻ってくる、おかえり！(パクリ)
codeをもってID Tokenをとりにいく。
取れたら、ログインできたことにしてしまうぞ
あー順番を書くだけで疲れたね、index.htmlを保存しておっぱいモミモミにいきたいね折角の週末だ。
今回実装するcodeフローのほかに、implicitフローというのもある。これはcodeではなくてユーザがID Tokenを持って戻ってくるものだと思えばよろしい。
codeを引き換えにID Tokenをとるフローが実装しにくい場合(サイトじゃなくて単発の画面コンテンツとか)にはこっちを使おう。
一方、AndroidやiOSのアプリみたいに、開発サイドから離れたところにアプリケーションを配布する場合はImplicitのフローを使おう。 これは、ImplicitのフローにはRPのパスワードに相当するクライアントシークレットを使わず、アプリケーションに値を埋める必要がないためである。
モバイルアプリケーションや画面コンテンツにシークレットを埋めると漏れるかもしれないからな。とりあえずシークレットが第3者に見えないようにすることがRPサイドのモラルであろう。(今回はダダ漏れだよ！)
フローの選択にあたり、ここは間違えないようにしたい。
いかん話がそれた。コード。

login.php
<?php
//このサンプルではサーバセッションでstateとnonceを管理する
session_start();
//IdP情報
$authorization_endpoint = "http://test.kirimanjirou.com:8080/openam/oauth2/authorize";
//RP情報
$client_id = "kirimanjirou003";
$redirect_uri = "https://kirimanjirou.com/sample/signup.php";
//stateの作成 特定文字+日付からランダムな32ケタを生成
$state =  substr((md5("manjistate" . date("YmdD His"))), 0, 32);
$_SESSION['state'] = $state;
//nonceの作成 作り方はstateと同じにした
$nonce = substr((md5("manjinonce" . date("YmdD His"))), 0, 32);
$_SESSION['nonce'] = $nonce;
//同意画面の表示方式を指定する
$display = "page";
//自作IdP側の再認証有無などをクライアントから指定できる
$prompt = "select_account";
//grantの指定を行う。今回はcode
$response_type = "code";
//scopeを指定する
$scope = array(
        "phone",
        "email",
        "address",
        "openid", //とりあえずこれがあればID Tokenは取れる
        "profile"
);
//httpクエリの作成
$query = http_build_query(array(
        "client_id"=>$client_id,
        "redirect_uri"=> $redirect_uri,
        "state"=>$state,
        "nonce"=>$nonce,
        "display"=>$display,
        "prompt"=>$prompt,
        "response_type"=>$response_type,
        "scope"=>implode(" ", $scope ),
));
//リダイレクト応答の作成
header( "Location: " . $authorization_endpoint . "?" . $query );
exit();
?>
view rawlogin.php hosted with ❤ by GitHub
ここはシンプルである。OpenID Connectで必要なパラメタを調べてくみ上げてリダイレクトするだけだ。
ほっとんどがパラメタのハメ込みだけ。

3.自作IdPからブッ飛ばされてきて、トークンをとりにいく
タイトルは長いが、処理は意外に楽チンコである。と思いきや一部うごいてねーんだが。

signup.php(前半)
<?php
//curlを使うぞ
$ch = curl_init();
//このサンプルではサーバセッションでstateとnonceを管理する
session_start();
$session_state = $_SESSION['state'];
$session_nonce = $_SESSION['nonce'];
//IdP情報
$token_endpoint = "http://test.kirimanjirou.com:8080/openam/oauth2/access_token";
$issue = "http://test.kirimanjirou.com:8080/openam";
$userinfo_endpoint = "http://test.kirimanjirou.com:8080/openam/oauth2/userinfo";
//RP情報、セッションに入れてもまぁいいのかもだがここは好みで静的に記載することにした
$client_id = "kirimanjirou003";
$client_secret = "kirimanjirou003";
$redirect_uri = "https://kirimanjirou.com/sample/signup.php";
//リクエストパラメタ
$req_code = $_GET['code'];
$req_state = $_GET['state'];
//stateの検証 セッションで引き継いだstateとリダイレクトされてきたパラメタは同じ？
if ($session_state != $req_state) { $NG_MSG = "state is NG"; }
//codeはちゃんとついてきているよね？
if ($req_code == null) { $NG_MSG = "code does not exsit"; }
//トークンエンドポイントリクエスト
//Header作成: client_id,client_secretを":"でつないでBASE64エンコードする
$client_cred = base64_encode( $client_id . ":" . $client_secret);
$headers = array(
        "Expect:",
        "Authorization: Basic " . $client_cred
        );
//パラメタ設定
$postdata = http_build_query(array(
        "grant_type"=>"authorization_code", //固定
        "redirect_uri"=>$redirect_uri,
        "code"=>$req_code
        ));
//token_endpointリクエスト作成
curl_setopt( $ch, CURLOPT_URL, $token_endpoint );
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
curl_setopt( $ch, CURLOPT_POST, 1 );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
//実行
$token_endpoint_result = json_decode( curl_exec( $ch ));
//ID Tokenをヴェリファーイｗｗｗｗしていく
$id_token = $token_endpoint_result->id_token;
$access_token = $token_endpoint_result->access_token;
~~~~
view rawsignup.php hosted with ❤ by GitHub

ここまでで、とりあえず実行してみるとID Tokenが取れていることがわかる。やったね！
これからID Tokenをヴェリファーイｗｗｗｗｗしていくのだ。
ところが、ここでコケてしもうた。Token署名の検証がうまくいかない。
IdPによってはクライアントシークレットで署名するものもあるのだが、OpenAMではID Tokenの署名シグネチャのKeyにサーバの鍵情報を使うように見える。そこで@ritou氏のページを参考に公開鍵を使ってシグネチャを作るのだ、と理屈まではわかって試してるんだがどうも動かない。
OpenAM側でどう署名しているのかわかるまで結構時間がかかっちまうな…ここは結構キモの部分だと思ってただけにすんなりいかないのは実に残念。だが、いったんギブアップとさせてくれ、すまない。
とりあえず、改ざんはされていない体で各claimsの検証をしていく。
signup.php(後半)
<?php
//しかたねえから手でちぎる
list($header, $payload, $token_signature) = explode('.', $id_token);
//BASE64デコードする
$header_cont = base64_decode(str_pad(strtr($header, '-_', '+/'), strlen( $header ) % 4, '=', STR_PAD_RIGHT));
$payload_cont = base64_decode(str_pad(strtr($payload, '-_', '+/'), strlen( $payload ) % 4, '=', STR_PAD_RIGHT));
$header_json = json_decode( $header_cont );
$payload_json = json_decode( $payload_cont );
//nonceの検証
if ( $session_nonce != $payload_json->nonce ) { $NG_MSG = "nonce is NG"; }
//issueの検証
if ( $issue != $payload_json->iss ) { $NG_MSG = "iss is NG"; }
//audの検証
if (true == is_array( $payload_json->aud ))
{
        if (true != in_array( $client_id,$payload_json->aud )) { $NG_MSG = "aud[] is NG"; }
}else{
        if ( $client_id != $payload_json->aud ) { $NG_MSG =  "aud is NG"; }
}
//azpの検証
if (array("azp",$payload_cont))
{
        if ($client_id != $payload_json->azp ) { $NG_MSG = "azp is NG"; }
}
if ( $NG_MSG == null)
{
        //userinfo_endpointリクエスト作成
        //Header作成:
        $ch = curl_init();
        $headers = array(
                "Expect:",
                "Authorization: Basic " . $access_token
                );
        curl_setopt( $ch, CURLOPT_URL, $userinfo_endpoint . "?access_token=" . $access_token );
        curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        //実行
        $userinfo_endpoint_result = json_decode( curl_exec( $ch ));
        //メインページへの情報引継ぎ
        $_SESSION['id_token_encode'] = $id_token;
        $_SESSION['access_token'] = $access_token;
        $_SESSION['id_token'] = $payload_json;
        $_SESSION['userinfo'] = $userinfo_endpoint_result;
        //リダイレクト応答の作成
        header( "Location: " . "https://kirimanjirou.com/sample/cont.php" );
        exit();
}else{
print<<<EOF
        <html>
        <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>ERROR</title>
        </head>
        <body>
        $NG_MSG
        </body>
        </html>
EOF;
}
?>
view rawsignup_2.php hosted with ❤ by GitHub
nonceの検証：
リプレイアタック防止である。これがうまく動いていると別ユーザがTokenEndpointにcodeを投げつけたとしても、クライアント側でそれを検知できる。ユーザセッションの値とID tokenの中身を比較することで成立するんだね。

issueの検証：
ID Toeknのclaimに含まれるIdPプロバイダのベースURLを検証する。クライアント側にベタ書きして比較すればOKだろう。OpenIDプロバイダ情報エンドポイントからあらかじめディスカバったりする場合は、それと比較しよう。

audienceの検証：
ID Toeknのclaimに含まれる値と自身のクライアントIDを比較する。@tkudos氏のTumblerでコメントいただいていたのでハマらずにすんだのだが、OpenAMではaud claimはひとつでも配列で返って来る。audとazpの扱いについては迷ったが今回は両方評価することに。azpをつけてこないIdPがいることやや、audが配列じゃない場合は想定しておくべきだろう。

時刻の検証：
今回は組み込んでない。IdPの示す発行日時が大幅に現在時刻とズレている場合は無効とする、など。

正常にIDトークンが検証できたのであれば、取れた情報をユーザセッションにぶち込んでしまおう。
ここではUserinfoエンドポイントも呼んで、ユーザ情報も取得する。
Userinfoエンドポイントにはアクセストークンを使うので、ほんとうはトークンが切れていたらリフレッシュトークンで再取得する処理を入れてもよいかもしれない。今回のケースではいらないし、ムラムラしてきたので今日はサボる。

4.ID Tokenを検証する
オサレなコンテンツページを作り、ID TokenのsubjectとUserInfoエンドポイントの内容を赤裸々に表示する。
RP_1
Oh…
cont.php
<?php
//curlを使うぞ
$ch = curl_init();
//タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');
session_start();
$id_token_encode = $_SESSION['id_token_encode'];
$id_token = $_SESSION['id_token'];
$access_token = $_SESSION['access_token'];
$userinfo = $_SESSION['userinfo'];
$NG_MSG = null;
$CONTENT = null;
if ( null == $_SESSION )
{
$NG_MSG = <<<EOF
セッションが無効です。
<a href = index.html>ログイン画面へ</a>
EOF;
}else{
$login_date = date("Y-m-d h:i:s",$id_token->iat);
$CONTENT = <<<EOF
ログインサービス: $id_token->iss<br>
ログイン日時: $login_date
<table border>
<tr> <td>USERID</td><td>$id_token->sub</td>
<tr> <td>お名前</td><td>$userinfo->name</td>
<tr> <td>TEL</td><td>$userinfo->phone</td>
<tr> <td>MAIL</td><td>$userinfo->email</td>
<tr> <td>ADDRESS</td><td>$userinfo->address</td>
</table>
<a href = "logout.php">ログアウトする</a>
<p>
IDTOKEN: $id_token_encode
EOF;
}
print<<<EOF
        <html>
        <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>TOP</title>
        </head>
        <body>
        <h1>自前IdPにログインしたか？</h1>
        $NG_MSG
        $CONTENT
        </body>
        </html>
EOF;
?>
view rawcont.php hosted with ❤ by GitHub

コンテンツページではセッションの中に有効なIDトークンが含まれているかを確認する。
必要であれば作ってきたphpにセッション確認の処理を追加して、二重にログイン画面が出ることをパスさせよう。複数のIdPを選択することを想定した場合、実装をサボらないほうがいいというのが個人的な意見である。IdPが複数選択できるシーンではきちんとログアウトの処理を実装しよう。
IdP側がログアウトしていたらRP側でも…というチェックセッションエンドポイントを試したかったが、OpenAMに投げるパラメタがわかんなかったので、保留。
5.ログアウトする
ログアウトボタンである。セッションデストローイｗｗｗｗだけでとりあえずいい。 今後OpenAMとのシングルログアウトを研究していこう。
<?php
session_start();
session_destroy();
print<<<EOF
        <html>
        <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>TOP</title>
        </head>
        <body>
        <h1>ログアウト</h1>
        <a href = index.html>ログイン画面へ</a>
        </body>
        </html>
EOF;
?>
view rawlogout.php hosted with ❤ by GitHub
おまけ
OpenAMのログインに利用したつくりは他のIdPでも動くんか？ということでgoogleのAPIを試してみた。
RP情報やIdP情報をカカッといじれば動くのか、というほど甘くはなく、
・googleはnonceを投げると怒る
・UserinfoエンドポイントではAuthorizationヘッダではなくクエリでアクセストークンを渡さないといけない
など、細かい部分はやっぱり違うようだ。今回のサンプルではloginとsignupの２つは個別に実装せにゃあならんかった。

以上である。
動くものは一応ココへ。
自作IdPは確認できないけどgoogleのほうは確認できるよ！やらなくてもいいけど。
とりあえずこんな感じ。一回作ってしまえばいろんなIdPで使いまわせるだろうし、正常ルートだけなら書く量もほとんどないね。

———————————–
＜OpenAMの今後のネタ、情報募集中＞
※シグネチャの検証を、とりあえず優先度高で調査する。
※displayには少なくともpopupパラメタに対応しているようだ。wapとtouchは試していない。
※promptの動作は確認できなかった。loginにしたところ認証画面が必ず出るとこまでは確認したが、そのあとにconsent_requiredのエラーになってしまって先に進めん。
※session_check_endpointにはOpenAMから共有するためのタネをもらっていないといけないはずなんだけど、見当たらない。id_tokenをPOSTするとRFCのsession managementに載ってるjspコンテンツがそのまま返ってくる、よくわからん。
※logout_endpointはid_tokenを受け取るようだが、error_description: “Unable to get SsoTokenManager”と出てしまい先に進まない。