今回はOpenAM+OpenIG+PolicyAgentなSSOをやってみます。
以下のような構成になります。
フローを簡単に説明すると
UserAgentがWebサーバにアクセス(初回)
Policy Agentがリクエストをインタラプト、Cookieが入っていないのでOpenAMに認証要求リダイレクト
OpenAMで認証後、Webサーバにリダイレクト
認証されている(=Cookieが入っている)のでPolicy AgentはCookie値をOpenAMに問い合わせて
ユーザ名/パスワードを取得
- Policy Agentが取得したユーザ名/パスワードをヘッダに入れてOpenIGにリクエスト
(パスワードは暗号化されている)
- OpenIGはログイン画面がリクエストされた時のみヘッダからユーザ情報を取得し、認証リクエストを行う
(通常のフォームPOSTを模倣する)
- Webアプリは認証リクエストを受け取り、ユーザ認証実行後OpenIG・Policy Agent経由で
UserAgentにレスポンスを返して認証終了
といった感じです。
OpenIGはリクエストのプロトコルやURI、ヘッダ等の情報やフォワードしたHTTPリクエストに対する
レスポンスの正規表現マッチングを使って処理を分岐させたり
HTTPクライアントとして動かしたりできるリバースプロキシソフトウェアになります。
今回はOpenIGを使ってOpenAMで認証されたユーザ名/パスワードでWebアプリにSSOするフローをやってみます。
※本記事ではOpenAMのユーザ名/パスワード=Webサイトのユーザ名/パスワードになることを想定しています。
参考URLは以下になります。
OpenAMによるシングルサインオン(2)リバースプロキシー編 - Tech-Sketch
Chapter 7. Tutorial On OpenAM Password Capture & Replay
Chapter 11. Configuration Templates
1. OpenIGのインストール
Javaのアプリケーションサーバで動作するらしいのでtomcat使ってやってみます。ダウンロードはここから→https://forgerock.org/downloads/openig-builds/
展開したwarファイルをtomcatのルートに設置
$ mv {展開したwar} /var/lib/tomcat7/webapps/ROOT.war
tomcat実行ユーザのホームを/etc/passwdで調べて以下のディレクトリに設定ファイルを作成
$ touch {tomcat実行ユーザのホーム}/.openig/config/config.json
あとはconfig.jsonの設定ファイルを書き換えていくだけでOK。(3を参照)
2. OpenAMの設定
Policy Agent編でのPolicy Agentの仕事はトークンの検証のみで良かったのですが今回はユーザ情報をOpenIGに送る必要がある(OpenIGからOpenAMのユーザ情報を取得できない)ことから
エージェントが”OpenAMからユーザ情報を取得する”設定に書き換える必要があります。
アクセス制御>対象のレルム>エージェント>対象のエージェント で
アプリケーションタブをクリックしてセッション属性処理を以下のように編集
アクセス制御>対象のレルム>認証 で
すべてのコア設定をクリックして認証ポストプロセスクラスを以下のように編集
次にPolicy Agent→OpenIG間の通信でユーザのパスワードを暗号化する為の対称鍵(DES)を作成します。
DESキー作成のためのモジュールはOpenAMに入っているので以下を実行して暗号化の対称鍵を生成
$ cd /var/lib/tomcat6/webapps/openam/WEB-INF/lib
$ java -classpath forgerock-util-1.3.0.jar:openam-core-12.0.0-SNAPSHOT.jar:
openam-shared-12.0.0-SNAPSHOT.jar com.sun.identity.common.DESGenKey
設定>サーバー及びサイト>対象のサーバ>高度 で
生成した暗号化キーを追加
また、XUIを使っているとパスワードがヘッダに付与されなかったりするみたいなので(参考URL)
設定>認証>コア のXUIインターフェースを無効にしてください。
3. OpenIGの設定
今回は超簡単に特定のIDのformタグがあればPolicy Agentから取得したユーザID/パスワードを自動的にPOSTして認証を行うような設定にします。
config.jsonは以下のとおり
{
"heap": {
"objects": [
{
"name": "HandlerServlet",
"type": "HandlerServlet",
"config": {
"handler": "FindLoginPageChain",
"baseURI": "http://localhost:5000"
}
},
{
"name": "FindLoginPageChain",
"type": "Chain",
"config": {
"filters": ["IsLoginPage", "FindLoginPage"],
"handler": "OutgoingChain"
}
},
{
"name": "IsLoginPage",
"type": "SwitchFilter",
"config": {
"onResponse": [{
"condition": "${exchange.isLoginPage.found == 'true'}",
"handler": "LoginChain"
}]
}
},
{
"name": "FindLoginPage",
"type": "EntityExtractFilter",
"config": {
"messageType": "response",
"target": "${exchange.isLoginPage}",
"bindings": [{
"key": "found",
"pattern": "<form\sid=\"loginform\"",
"template": "true"
}]
}
},
{
"name": "LoginChain",
"type": "Chain",
"config": {
"filters": ["CryptoHeaderFilter", "LoginRequest"],
"handler": "OutgoingChain"
}
},
{
"name": "CryptoHeaderFilter",
"type": "CryptoHeaderFilter",
"config": {
"messageType":"REQUEST",
"operation":"DECRYPT",
"algorithm":"DES/ECB/NoPadding",
"key":"********",
"keyType":"DES",
"charset":"utf-8",
"headers": ["password"]
}
},
{
"name": "LoginRequest",
"type": "StaticRequestFilter",
"config": {
"method": "POST",
"uri": "http://localhost:5000/login",
"form": {
"username":["${exchange.request.headers['username'][0]}"],
"password":["${exchange.request.headers['password'][0]}"],
"submit":["送信"]
}
}
},
{
"name": "OutgoingChain",
"type": "Chain",
"config": {
"filters": ["CaptureFilter"],
"handler": "ClientHandler"
}
},
{
"name": "CaptureFilter",
"type": "CaptureFilter",
"config": {
"captureEntity": true,
"file": "/tmp/openig.log"
}
},
{
"name": "ClientHandler",
"type": "ClientHandler",
"config": {
}
}
]
},
"servletObject": "HandlerServlet"
}
シーケンスはこんな感じ(テキトーです)
CryptoHeaderFilterのkeyには2で作成した暗号化キーを設定してください。
上記設定ではhtmlの正規表現パターンマッチによりログイン画面の判定をしていますが
URLでの判定も可能なので、リクエストしたURLに応じて処理を分けるのも有りです。
また、今回は必要最低限のフォームデータをPOSTしていますが
通常formにはCSRFトークン等のnonceがCookieあるいはhiddenに埋め込まれていると思うので
その値を${exchange.response}のプロパティや正規表現を使ってセットする必要があります。
OpenIGからデータベースやテキストファイルにアクセスすることも可能なので
WebアプリとOpenAMのログインユーザのマッピングも可能です。
※通常のフォームPOSTなのでOpenAMのパスワードを利用しない場合は
Webアプリのユーザのパスワードを暗号化するなりしてどこかに保存しておかなければなりません。
config.jsonは複雑なので参考URLのChapter 11. Configuration Templatesをベースに作成 すると楽です。
4. Webサーバの設定
Apacheはポート80のリクエストをポート8080のOpenIG(tomcat)にフォワーディングする設定にします。ProxyPass / http://localhost:8080/
Policy Agentのインストール・設定は以前の記事を参照。
Python/Flaskのサンプルアプリは以下の通り。
ログイン画面出して認証するだけのプログラムです。面倒だったのでセッション立ててませんw
agent.py
# -*- coding: utf-8 -*-
from flask import Flask, request, render_template
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
elif request.method == "POST":
if request.form["username"] == "hoge" and request.form["password"] == "fuga":
return "you are authenticated"
return "bad request"
app.debug = True
app.run()
templates/login.html
<!DOCTYPE html>
<html>
<head></head>
<body>
<form id="loginform" action="/login" method="post">
<table>
<tr>
<td>username:</td>
<td>
<input type="text" name="username" />
</td>
</tr>
<tr>
<td>password:</td>
<td>
<input type="password" name="password" />
</td>
</tr>
</table>
<input type="submit" value="送信" name="sbmt" />
</form>
</body>
</html>
あとはagent.pyを稼働させてWebサイト(/login)にアクセスすると、クライアント側からは
Webサイトにアクセスする→OpenAMにリダイレクトして認証→Webサイトに自動認証
という流れで画面遷移し、SSOが実現できます。
OpenIG+Policy AgentはPolicy Agentのみのパターンとやっていることは同じですが
Policy AgentのパターンでWebアプリを改修しないといけない部分を
OpenIGが担っているのでWebサイト側は基本的には改修不要になります。
ただし上記例はWebとOpenAMのユーザ名とパスワードが一致している必要があり
一致していない場合はDBでマッピングする、Webアプリ内からOpenAMのREST API叩く等の
仕組みを検討する必要があります。