昨年シングルサインオンプラグインの設計について考えた際、「フックによる拡張性の担保」について言及しました。これまで関わったPowerCMS X + SAML認証の案件では事前になんらかの方法でユーザーを登録しておく仕様だったのですが、singlesignon_pre_login.saml
フックとSAML Response内にあるデータを利用し、新規ユーザーの場合はユーザー登録処理を実行するように試験実装してみました。
ユーザーデータの例
オープンソースの認証・アクセス管理基盤である「Keycloak」を用いて開発していますので、KeycloakのIdPの設定でプロトコル・マッパー設定を行いました。その結果、SAML Responseのsaml:AttributeStatement要素にユーザーデータが格納されるようになりました。
<saml:AttributeStatement>
<saml:Attribute FriendlyName="organizationUnitName" Name="urn:oid:2.5.4.11" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">PowerCMS X開発部</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Hideki</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute FriendlyName="surname" Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Abe</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute FriendlyName="locale" Name="locale" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">ja</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
PowerCMS Xプラグインの実装
シングルサインオンプラグインとは別のプラグインを新たに用意し、singlesignon_pre_login.saml
フックでログイン処理の前に実行する処理を書きました。具体的には、SAML Responseの内容を解析してデータを取得・マッピングし、ユーザーモデルと権限モデルに新たなオブジェクトを登録してログに記録する処理です。ユーザーモデルと権限モデルも記事モデル等と性質は同じですので「PHPによるプログラミング・ガイド (データベース編) | PowerCMS X」に沿ってオブジェクトを操作するコードを書けば大丈夫です。
コードよりもユーザー作成・権限の割り当ての具体的な仕様を決める方が難しいかもしません。Identify Providerにあるデータも含めて注意深く検討する必要があるでしょう。所属組織と役職に基づきワークスペースとロールを割り当てる仕組みを作る事はできそうですが、例外事項(所属組織と役職以外の考慮事項)が増えると手動対応が必要になることが予想されます。
- どのようなユーザー名にするのか(メールアドレスにするのも可であるし、なんらか自動生成することも考えられる)
- どのワークスペースに権限を与えるのか
- どのロールを割り当てるのか
コードは以下のようになりました。
public function regist_user_by_saml( $app ) {
$email = $app->response_parser->extract_identifier();
$sure_name = null;
$given_name = null;
$organization_unit = null;
$locale = null;
$query = './/samlp:Response/saml:Assertion/saml:AttributeStatement/saml:Attribute';
$saml_attributes = $app->response_parser->xpath->query( $query, $app->response_parser->doc );
foreach ( $saml_attributes as $saml_attribute ) {
// phpcs:disable WordPress.NamingConventions.ValidVariableName
switch ( $saml_attribute->getAttribute( 'Name' ) ) {
case 'urn:oid:2.5.4.4':
$sure_name = $saml_attribute->firstChild->textContent;
break;
case 'urn:oid:2.5.4.42':
$given_name = $saml_attribute->firstChild->textContent;
break;
case 'urn:oid:2.5.4.11':
$organization_unit = $saml_attribute->firstChild->textContent;
break;
case 'locale':
$locale = $saml_attribute->firstChild->textContent;
break;
}
// phpcs:enable
}
$user = $app->db->model( 'user' )->get_by_key( [ 'email' => [ 'BINARY' => $email ] ] );
if ( $user->id ) {
return;
}
// ユーザー登録
$user_name = 'TEST001'; // NOTE: テスト用暫定値。実際には自動生成するなど。
$user->name( $user_name );
$user->nickname( $sure_name . ' ' . $given_name );
$user->email( $email );
$user->password( password_hash( 'DUMMY', PASSWORD_BCRYPT ) );
$user->language( $locale );
$user->created_by( 0 );
$user->created_on( $app->date( 'YmdHis' ) );
$user->status( 2 );
$save_result = $user->save();
if ( ! $save_result ) {
$app->error( 'User registration failed.' );
exit;
}
// 権限設定
$role_id = 3; // NOTE: テスト用暫定値
$workspace_id = 4; // NOTE: テスト用暫定値
$permission = $app->db->model( 'permission' )->new();
$permission->user_id( $user->id );
$permission->workspace_id( $workspace_id );
$save_result = $permission->save();
if ( $save_result ) {
$app->set_relations(
[
'from_id' => $permission->id,
'name' => 'roles',
'from_obj' => 'permission',
'to_obj' => 'role',
],
[ $role_id ]
);
} else {
$app->error( 'User registration failed.' );
exit;
}
$log_message = $app->translate(
"Added user 「%s」(ID:%s) with SAML authentication.",
[
$user_name,
$user->id,
]
);
$app->log(
[
'message' => $log_message,
'category' => 'save',
'model' => 'user',
'level' => 'info',
]
);
}
操作結果
以下の画面キャプチャのように、初めてログインする際はログイン前にユーザー登録処理が行われ「Added user 「ユーザー名」〜」のようなログが残りました。2回目からはユーザー登録処理をスキップしてログイン処理だけが行われました。