openssosampelsのスタンドアロン版サンプルクライアントで動作確認

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

昨日(id:t_yamo:20070412)までの作業でOpenSSOのサーバーが動作するので、それをサンプルクライアントで確認してみる。
同じく昨日(id:t_yamo:20070412)準備したopenssosamplesの中にLogin.classというスタンドアロン版サンプルクライアントが存在するので、それを使う。

  1. scriptsの親フォルダ(注:scriptsの中に入では駄目)をカレントフォルダにし、「scripts/Login.sh」を実行(実行権限がないときは付与)。まずは接続先について聞かれるので以下のように答えていく。
    • Realm (e.g. /):「/」もしくは「opensso
    • Login module name (e.g. DataStore or LDAP):「DataSotre」
  2. 正常に接続できた場合には「DataStore: Obtained login context」と表示され、認証するユーザについて聞かれるので以下のように答えていく。
    • User Name:「amAdmin」
    • Password:「password」
  3. 認証も成功した場合は「Login succeeded.」と表示される。ただ、すぐにログアウトする実装になっているので、同時に「Logged Out!!」も表示される。認証に失敗した場合には「Login failed.」とのみ表示される。


上記の「Realm」のところはSSOサーバーの管理画面の「Access Control」タブの「Realms」に設定されているアイテムを指定する。
「/」だとデフォルトになるっぽい。
インストール直後は「opensso」しかないので、「/」と「opensso」のどちらを指定しても同じっぽい。


上記の「Login module name」のところはSSOサーバーの当該Realm(上記のopenssoという名前をクリックしたところ)の「Authentication」タブの「Module Instances」に設定されているアイテムを指定する。
LDAPは用意していないのでファイルベースのDataStoreを選択している。


上記の認証のところは当該Realmの「Subjects」タブの「User」タブに設定したユーザを指定する。
ただし、これは設定によりけりなので後でLDAPやらJDBCやら経由でユーザを取得するようにした場合はそちらに存在するデータを指定することになる。
なお、「User」タブにユーザを追加すると「/home/tomcat5/opensso/idRepo/user」にファイルが生成される。
この「idRepo」の場所は当該Realmの「Data Sotres」タブの「files」の「Files Repository Directory」の値で決まるようだ。


うまく接続できない場合はopenssosamplesの中の「debug」(クライアント側)か「/home/tomcat5/opensso」の中の「debug」(サーバー側)を調べる。
なお、それぞれのAMConfig.propertiesの「com.iplanet.services.debug.level」を変更すればログレベルを変えられる。

SSOクライアントの作成(1)

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

ようやく準備が整ったのでSSOクライアントを作成していく。


SSOの実現形態として「エージェント型」「リバースプロキシ型」がある。
OpenSSOは「リバースプロキシ型」にも対応しているが「エージェント型」がベースらしい。
「エージェント型」にするにしても、実際にSSOエージェントを立てる場合と、SSOクライアント(Webアプリケーション)で直接APIを呼び出す場合があるらしい。


参考1
OpenSSO の技術概要からわかること - tkudo's weblog about identity management
参考2
Sun Java System Access Manager


今回はエージェントを立てずにSSOクライアントで直接APIを呼び出し、SSOセッションをCookieで持ちまわる形で実装する。
また、ログイン画面については個々のSSOクライアントのものを使用し、SSOクライアントのログインプロセスの中でSSOの認証を行うこととする。


まず、実装については以下のページが参考になる。


参考3
Developer Forums
参考4
Chapter 5 Single Sign-On And Sessions
参考5
Using OpenSSO authentication with JBoss SEAM - Strange Brew


基本的にはamclientsdk.jarとAMConfig.propertiesがあればよいらしい。
ひとまずEclipse等でJavaプロジェクトを作成し、openssosamples/classes直下のプロパティファイル全てををソースフォルダ直下にコピーする。
また、openssosamples/WEB-INF/lib直下のjarファイル全てをライブラリフォルダ直下にコピーする(j2ee.jarが競合する場合は省く)。


今回のケースではSSO固有の実装は以下の部分。

  • SSOクライアントのログインプロセス内でSSO認証を行う
  • SSOクライアントのログアウトプロセス内でSSOログアウトを行う
  • SSOクライアントのリクエスト毎にSSOが有効か否かをチェックする

3番目のところでSSOセッションがタイムアウトするのを防ぐのにリフレッシュが必要。

SSOクライアントの作成(2)ログインプロセス内でのSSO認証

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

以下、ドメインは「example.com」、SSO用のCookieの名前は「ssotoken」とする。
なお、ここで示す実装はSSOクライアントとして共通というわけではなく、当方の要件に合わせたものであるため、この実装を適用する際には事前の検討が必要である。


まず、ログインプロセスが始まる段階でCookieを無効にする。
これは、ログインを試みた段階でそのWebクライアント(SSOクライアントではなく、ブラウザがあるホストのこと)をSSOセッションと切り離すため。
これがないと、そのアプリケーションに対する認証に失敗してもSSOセッションにより「他のアプリケーションでログインが成功している」とみなされて、失敗前に有効だったユーザでログインできてしまう。

public static final String SSOTOKEN_NAME = "ssotoken";
public static final String DOMAIN_NAME = "example.com";
... 略 ...
	// 
	// FIXME:requestとしてHttpServletRequestを取得
	// 
	Cookie cookie = new Cookie(SSOTOKEN_NAME, "");
	cookie.setDomain(DOMAIN_NAME);
	response.addCookie(cookie);
... 略 ...


次に、SSOクライアント自体のログインプロセスを実行する。
ここは各SSOクライアントごとの実装。
ただし、認証を全面的にSSOに任せる場合はこの部分は不要(というかSSOを導入するならSSO一本で認証する方が妥当な気がする)。


SSOクライアント自体の認証をパスした場合、SSOのログインプロセスを実行する。
ここは上記参考5かopenssosamplesのLogin.classと同じような実装になる。

// 
// FIXME:LOGとしてログ用インスタンスを取得
// 
public static final String USER_ID = "USER_ID";
public static final String PASSWORD = "PASSWORD";
public static final String SSOTOKEN_NAME = "ssotoken";
public static final String DOMAIN_NAME = "example.com";
public static final String SET_COOKIE_REQUEST = "SET_COOKIE_REQUEST";
public void login() {
	// SSOクライアントの認証
	... 略 ...
	// 
	// FIXME:requestとしてHttpServletRequestを取得
	// 
	// SSO認証
	try {
		request.setAttribute(USER_ID, id);
		request.setAttribute(PASSWORD, password);
		if (!ssologin()) {
			// SSO認証に失敗。
			//
			// FIXME:ここでSSOクライアント自体のログアウトを行う。
			//
			throw new Exception("error");
		}
	} catch(AuthLoginException e) {
		LOG.error("AuthLogin problem", e);
		throw new Exception("error");
	} catch(UnsupportedCallbackException e) {
		LOG.error("Callback problem", e);
		throw new Exception("error");
	}
	... 略 ...
}
public boolean ssologin()
throws UnsupportedCallbackException, AuthLoginException {
	AuthContext lc = getAuthContext();
	boolean succeed = false;
	Callback callbacks = null;
	// get information requested from module
	while (lc.hasMoreRequirements()) {
		callbacks = lc.getRequirements();
		if (callbacks != null) {
			addLoginCallbackMessage(callbacks);
			lc.submitRequirements(callbacks);
		}
	}
	if (lc.getStatus() == AuthContext.Status.SUCCESS) {
		LOG.info("Login succeeded.");
		succeed = true;

		try {
			setSSOTokenIdToCookie(lc);
		} catch (Exception e) {
			LOG.info("Could not get SSOToken.");
			succeed = false;
		}

	} else if (lc.getStatus() == AuthContext.Status.FAILED) {
		LOG.error("Login failed.");
	} else {
		LOG.error("Unknown status: " + lc.getStatus());
	}
	return succeed;
}
public void addLoginCallbackMessage(Callback callbacks)
throws UnsupportedCallbackException {
	int i = 0;
	try {
		for (i = 0; i < callbacks.length; i++) {
			if (callbacks[i] instanceof TextOutputCallback) {
				// N/A
			} else if (callbacks[i] instanceof NameCallback) {
				handleNameCallback*1;
	}
}
public void handleNameCallback(NameCallback nc)
throws IOException {
	// 
	// FIXME:requestとしてHttpServletRequestを取得
	// 
	String name = (String) request.getAttribute(USER_ID);
	nc.setName(name);
}
public void handlePasswordCallback(PasswordCallback pc)
throws IOException {
	// 
	// FIXME:requestとしてHttpServletRequestを取得
	// 
	String passwd = (String) request.getAttribute(PASSWORD);
	pc.setPassword(passwd.toCharArray());
}
public void setSSOTokenIdToCookie(AuthContext lc)
throws Exception {
	// 
	// FIXME:responseとしてHttpServletResponseを取得
	// FIXME:requestとしてHttpServletRequestを取得
	// 
	SSOToken token = lc.getSSOToken();
	String tokenId = token.getTokenID().toString();
	Cookie cookie = new Cookie(SSOTOKEN_NAME, tokenId);
	cookie.setDomain(DOMAIN_NAME);
	response.addCookie(cookie);
	// この処理を実行したリクエスト内にはCookieが無いので
	// request属性としてSSOが有効であることを示す
	request.setAttribute(SET_COOKIE_REQUEST, tokenId);
}

今回、SSOのログインプロセスが失敗した場合はSSOクライアント側もログアウトしてセッションを無効にしているが、要件によっては「SSOセッションは無効だが当該Webアプリケーションは利用可能」というケースがあるかもしれない。

※注:CookieのDomainやPathは適宜設定するように。

*1:NameCallback)callbacks[i]); } else if (callbacks[i] instanceof PasswordCallback) { handlePasswordCallback((PasswordCallback)callbacks[i]); } else { throw new UnsupportedCallbackException(callbacks[i]); } } } catch (IOException e) { LOG.error("Login Failed", e); throw new UnsupportedCallbackException(callbacks[i],e.getMessage(

SSOクライアントの作成(3)ログアウトプロセス内でSSOログアウトを行う

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

ここまで書いて思ったのだけれど、SSO(シングルサインオン)関連のキーワードが全然自動リンクにならない。
SSOに関する言及はあまりないのね。
SSOを使いたいという要件がないのかなぁ。


閑話休題


今回の要件では個別のSSOクライアントでログアウトした場合はSSOセッションも無効化する。
ただし、個別のSSOクライアントのセッションがタイムアウトしたタイミングでのSSOセッションの無効化は行っていない。
SSOクライアントのセッションがSSOセッションよりも短い場合、SSOクライアントのセッションスコープに保持した値が失われているにも関わらずユーザーがログインできてしまうため、処理に不整合が発生し得る。

これを回避するには以下のいずれかの方法を実施する必要があるので注意すること。



追記:考えてみるとSSOはリフレッシュされるのでタイムアウト時間を長く設定することは無理そう。また、個別アプリのタイムアウトのタイミングでSSOセッションを無効にすると、他のアプリを操作しているにもかかわらずタイムアウトになってしまう。このあたりは一般的にはどうするんだろう。


で、本題のログアウトの実装については以下のようになる。

// 
// FIXME:LOGとしてログ用インスタンスを取得
// 
public static final String SSOTOKEN_NAME = "ssotoken";
public static final String SET_COOKIE_REQUEST = "SET_COOKIE_REQUEST";
public static final String MODULE_NAME = "DataStore";
public void logout() {
	// SSOクライアントのログアウト
	... 略 ...
	try {
		SSOTokenManager ssoManager = SSOTokenManager.getInstance(); 
		SSOToken ssoToken = ssoManager.createSSOToken(getSSOTokenIdFromCookie()); 
		AuthContext lc = new AuthContext(ssoToken);
		AuthContext.IndexType indexType = AuthContext.IndexType.MODULE_INSTANCE;
		lc.login(indexType, MODULE_NAME);
		lc.logout();
	} catch (Exception e) {
		LOG.error("Logout fail.", e);
		throw new Exception("error");
	}
}
public String getSSOTokenIdFromCookie() {
	// 
	// FIXME:requestとしてHttpServletRequestを取得
	// 
	// このリクエスト内でSSOセッションを生成した場合はrequest内にCookieが
	// ないため、予めrequest属性に設定した値を取得する
	String tokenId = (String) request.getAttribute(SET_COOKIE_REQUEST);
	if (tokenId != null) {
		return tokenId;
	}
	// CookieからSSOTokenのIDを設定
	Cookie[] cookies = request.getCookies();
	Cookie cookie = null;
	for (int i = 0;i < cookies.length;i++) {
		if (cookies[i].getName().equals(SSOTOKEN_NAME)) {
			cookie = cookies[i];
			break;
		}
	}
	return cookie.getValue();
}

SSOTokenを渡してAuthContextを生成しているが、これが本当に正しいのかどうかは謎。

SSOクライアントの作成(4)リクエスト毎にSSOが有効か否かをチェックする

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

リクエスト毎にSSOクライアントとSSOの認証が済んでいるか否かをチェックする。
今回は以下のような実装になっている。

  • SSOクライアント未認証 SSO未認証
    • エラー(1)
  • SSOクライアント認証済 SSO未認証
    • エラー(2)
  • SSOクライアント未認証 SSO認証済
    • SSOクライアントでパスワードなし認証(=ユーザの存在チェック)
      • SSOクライアントで認証OK
        • SSOクライアントを認証済にしてSSOセッションをリフレッシュして正常系へ(3-1)
      • SSOクライアントで認証NG
        • エラー(3-2)
  • SSOクライアント認証済 SSO認証済
    • SSOセッションをリフレッシュして正常系へ(4)
// SSOクライアントとSSOで認証済ならtrue、それ以外はfalseを返す
public boolean isValidLoginStatus() {
	try {
		SSOTokenManager ssoManager = SSOTokenManager.getInstance(); 
		SSOToken ssoToken = ssoManager.createSSOToken(SSOUtils.getSSOTokenIdFromCookie()); 
		if (isValidLoginForLocalApp()) {
			// このアプリケーションで未ログインの場合は
			// SSOでログインチェックを行う。
			if(ssoManager.isValidToken(ssoToken)) {
				// SSOでログイン済みの場合はこのアプリケーションでも
				// ログイン済みにしてから正常系で返す。
				if(login4validsso(ssoToken.getProperty("UserId"))) {
					// SSOでログインしていたのでこのアプリでもログインした
					ssoManager.refreshSession(ssoToken);
					return true; // (3-1)
				} else {
					// SSOでログインしているがこのアプリのユーザではない
					return false; // (3-2)
				}
			} else {
				// このアプリでもSSOでも未ログイン
				return false; // (1)
			}
		}
		// このアプリでログイン済み
		// ログイン済みの場合でもidle時間を0にするために
		// SSOサーバーにアクセスする。
		ssoManager.refreshSession(ssoToken); 
		return true; // (4)
	} catch (SSOException e) {
		LOG.error("Validation fail.", e);
		return false; // (2)
	} 
}
public boolean isValidLoginForLocalApp() {
	// SSOクライアント自体が認証済か否かチェックし、
	// 認証済であればtrue、そうでなければfalseを返す
	... 略 ...
}
public void login4validsso(String id) {
	// 前述のlogin()とほぼ同じ動作。
	// ただし、SSOクライアントに対するパスワード認証がないのと
	// SSO認証はしない(もう認証済)ところが異なる。
	... 略 ...
}

と、いうことでおよその実装が完了。
ただし、全般的にテストコードなので注意。


一段落。
でも、自分のところの要件ではPHPでも対応する必要があるんだな。これが。
OpenSSO Extensionに「SAML 2.0 PHP Relying Party(旧lightbulb)」があるのだけれど、PHP5用っぽいんだよねぇ。

アカウント管理にJDBCを使う

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

SSOサーバーの管理画面の「Subjects」でユーザをコツコツいじるのはさすがに辛いので、DB上にあるユーザーを参照することにする。
今回はHSQLDBを参照するように設定してみる。

ちなみにDBレイアウトは以下のような感じ。

CREATE TABLE user (
	id VARCHAR(13),
	type VARCHAR(1),
	groupid VARCHAR(6),
	userid VARCHAR(6),
	name VARCHAR(40),
	password VARCHAR(40),
	deleterid VARCHAR(13),
	modifierid VARCHAR(13),
	tstamp TIMESTAMP,
	PRIMARY KEY (
		id
	)
);
  1. SSOサーバーの管理画面にログインする。
  2. Access Control」タブの「opensso」Realmの「Authentication」タブを開き、「Module Instances」の「New」ボタンを押す。
  3. 「Name」に「test」と入力し、「Type」として「JDBC」を選択して「OK」ボタンを押す。
  4. 「Module Instances」に「test」が追加されているので(追加されてない場合は画面を切り替えると出てくるかも)、「test」をクリックする。
  5. ひとまず以下のように設定して「OK」ボタンを押す。Passwordが未入力だと例外が発生するため、半角スペースを入れてあるところに注意。実際にはPasswordが空でも半角スペースを指定してログインできるらしい。
    • Connection Type:「Non-persistent JDBC connection」
    • Connection Pool JNDI Name:「java:comp/env/jdbc/samplePool」(使わないけどデフォルトのまま)
    • JDBC Driver:「org.hsqldb.jdbcDriver」
    • JDBC URL:「jdbc:hsqldb:hsql://localhost:9001」
    • Connect This User to Database:「sa」
    • Password for Connecting to Database:「 」(半角スペース1つ)
    • Password for Connecting to Database (confirm):「 」(半角スペース1つ)
    • Password Column String:「password」
    • Prepared Statement:「select password from user where id = ?」
    • Class to Transform Password Syntax:「com.sun.identity.authentication.modules.jdbc.ClearTextTransform」(デフォルトのまま)
    • Authentication Level:「0」(デフォルトのまま)


ここまでの設定をすることで、SSOクライアントのAuthContext#loginの第二引数に「test」と指定したときにHSQLDB上の「user」テーブルを見に行くようになる。
ただし、これでもまだSSOサーバーの管理画面の「Subjects」の設定がないとSSO認証されない。
上記に加えて以下の設定が必要になる。

  1. Access Control」タブの「opensso」Realmの「Authentication」タブを開き、「General」の「Advanced Properties」ボタンを押す。
  2. 「Realm Attributes」の「User Profile」で「Ignored」を選択。
  3. 「Realm Attributes」の「Generate UserID Mode」で「Enabled」のチェックを外す。
  4. 「Save」ボタンを押す。


ちなみにこれらの設定がちゃんとできているかどうかの確認はSSOクライアントを使うのではなくopenssosamplesのLogin.classを使うと楽である。
これまで「DataStore」を指定していたところを「test」にすればよい。
なお、DB上に格納されているパスワードが平文でない場合は「Class to Transform Password Syntax」に適切なクラスを指定すればよさそう。

OpenSSOで使うCookieの名前

2008/11/21:この記事にあるOpenSSOはかなり古いものです。最近のものを使う際の参考にはならないかもしれませんのでご注意ください。

AMConfig.propertiesを見ると「com.iplanet.am.cookie.name」が「iPlanetDirectoryPro」になっているので、Cookieの名前は「iPlanetDirectoryPro」にした方が他のアプリケーションと相性がよくなるかも。
他にも「com.iplanet.am.pcookie.name」というのもあるが、謎。