2014-07-21

OpenAMでSSO【OpenIG編】

今回はOpenAM+OpenIG+PolicyAgentなSSOをやってみます。

以下のような構成になります。

openig-architecture

 

 

フローを簡単に説明すると

  1. UserAgentがWebサーバにアクセス(初回)

  2. Policy Agentがリクエストをインタラプト、Cookieが入っていないのでOpenAMに認証要求リダイレクト

  3. OpenAMで認証後、Webサーバにリダイレクト

  4. 認証されている(=Cookieが入っている)のでPolicy AgentはCookie値をOpenAMに問い合わせて

ユーザ名/パスワードを取得

  1. Policy Agentが取得したユーザ名/パスワードをヘッダに入れてOpenIGにリクエスト

(パスワードは暗号化されている)

  1. OpenIGはログイン画面がリクエストされた時のみヘッダからユーザ情報を取得し、認証リクエストを行う

(通常のフォームPOSTを模倣する)

  1. Webアプリは認証リクエストを受け取り、ユーザ認証実行後OpenIG・Policy Agent経由で

UserAgentにレスポンスを返して認証終了

といった感じです。

 

OpenIGはリクエストのプロトコルやURI、ヘッダ等の情報やフォワードしたHTTPリクエストに対する

レスポンスの正規表現マッチングを使って処理を分岐させたり

HTTPクライアントとして動かしたりできるリバースプロキシソフトウェアになります。

 

今回はOpenIGを使ってOpenAMで認証されたユーザ名/パスワードでWebアプリにSSOするフローをやってみます。

※本記事ではOpenAMのユーザ名/パスワード=Webサイトのユーザ名/パスワードになることを想定しています。

 

参考URLは以下になります。

OpenAMによるシングルサインオン(2)リバースプロキシー編 - Tech-Sketch

OpenIG | LAJ技術ブログ

Chapter 5. Detailed Use Cases

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からユーザ情報を取得する"設定に書き換える必要があります。

 

アクセス制御>対象のレルム>エージェント>対象のエージェント で

アプリケーションタブをクリックしてセッション属性処理を以下のように編集

openam-openig-setting1

 

アクセス制御>対象のレルム>認証 で

すべてのコア設定をクリックして認証ポストプロセスクラスを以下のように編集

openam-openig-setting2

openam-openig-setting3

 

次に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

 

設定>サーバー及びサイト>対象のサーバ>高度 で

生成した暗号化キーを追加

openam-openig-passwd

 

また、XUIを使っているとパスワードがヘッダに付与されなかったりするみたいなので(参考URL

設定>認証>コア のXUIインターフェースを無効にしてください。

openam-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"
}

 

シーケンスはこんな感じ(テキトーです)

openig-sequence

 

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叩く等の

仕組みを検討する必要があります。

 

このエントリーをはてなブックマークに追加