ある案件で利用しているPowerCMS Xの会員サイト(Membersプラグインを使用)においてSAML認証を利用したい、という依頼を受けプラグイン実装を行いました。SAMLはシングルサインオン(1回の認証で複数のWebアプリケーションなどが利用可能になる)のプロトコルですが、関連する情報等を読んでいると他にもOpenID Connectのようなプロトコルもあることを知りました。折角プラグインを作成するならある程度汎用的なプラグインにできないか、と考え始め設計をしてみました。
プラグインの構成の検討
初めはSAML認証をするときにアクセスするpt-member-saml.php
からPTMemberSSOクラスを呼び出して認証作業ができるように考えていました。ここで、先に書いたOpenID Connectの存在、そして会員サイトのメンバーを管理しているメンバーモデルだけでなくPowerCMS Xのユーザーを格納しているユーザーモデルへの対応、が思い浮かびました。ステイホームで土・日に時間があったので検討した結果、次のような構成になりました。
まず、SAML・OpenID Connectの両方で必要な処理をPTSingleSignOnクラスに実装します。
class PTSingleSignOn extends Prototype {
public $cookie_name = 'pt-user';
protected $model = 'user';
public function __construct ( array $options = [] ) {
$this->id = 'SingleSignOn';
$this->redirect_url_on_error = $this->admin_url;
if ( count( $options ) > 0 ) {
foreach ( $options as $key => $value ) {
if ( property_exists( 'PTSingleSignOn', $key ) ) {
$this->$key = $value;
}
}
}
parent::__construct( $options );
}
protected function login_with_sso(
string $auth_type,
string $auth_key,
string $auth_value,
string $model = 'user',
?string $return_url = null,
array $terms = [],
int $workspace_id = 0
) {
}
protected function make_identifier( string $text = '' ): string {
$app = $this;
$id = 'pcmsx_' . bin2hex( random_bytes( 24 ) );
$expires = $app->singlesignon_max_wait_time;
$session = $app->db->model( 'session' )->get_by_key([
'name' => $id,
'kind' => 'AN'
]);
$session->text( $text );
$session->start( $_SERVER['REQUEST_TIME'] );
$session->expires( $_SERVER['REQUEST_TIME'] + $expires );
$session->save();
return $id;
}
protected function check_component( string $key ): object {
$app = $this;
$component = $app->component( $key );
if ( ! is_object( $component ) ) {
$app->error( '%s plugin is disabled or not found.', [ $key ] );
exit;
}
return $component;
}
}
SAMLによる認証を実装するPTSAMLクラスは、PTSingleSignOnクラスを継承して実装します。
class PTSAML extends PTSingleSignOn {
private function attempt_login( string $response, string $relay_state ): void {
$this->login_with_sso( 'SAML', 'email', $identifier, $this->model, $return_url, [], $workspace_id );
}
public function run(): void {
}
}
OpenID Connectによる認証を実装するPTOIDCクラスも、PTSingleSignOnクラスを継承して実装します。
class PTOIDC extends PTSingleSignOn {
private function attempt_login(): void {
$this->login_with_sso( 'OpenID', 'email', $email, $this->model, $return_url, [], $workspace_id );
}
public function run(): void {
}
}
これによりプロトコルが異なる場合でもコードが重複することもなく、保守性が良いと考えています。また、クッキー名(cookie_name
)とモデル名(model
)を切り替えることで、メンバーモデルにも対応ができました。
フックによる拡張性の担保
OpenID Connectについて調べていると、さまざまなOpenID Providerが存在することを知りました。例えばGoogle、Yahoo!、LINEなどです。OpenID Connectとは知らず利用していたのでしょう。会員サイトだと「複数のOpenID Providerを利用したい」という要望もありえるかもしれません。そこで、複数のOpenID Providerを登録したいときは別プラグインで拡張してもらうのが一案か、と考えました。別プラグインにOpenID Providerの設定を書く、もしくはいっそOpenID Providerを管理するモデルを作成する、ということも可能です。
フックポイントを用意するのは簡単で、フック名を決めて下記コードをrunメソッドの最初の辺りに置きました。
if ( ! empty( $app->hooks ) ) {
$app->run_hooks( 'singlesignon_pre_run' );
}
複数のOpenID Providerを管理するプラグインのconfig.json
には以下を記述します。
{
"label" : "SingleSignOnExtend",
"id" : "singlesignonextend",
"component" : "SingleSignOnExtend",
"version" : "0.6",
"hooks": {
"singlesignonextend_pre_run": {
"singlesignon_pre_run": {
"component": "SingleSignOnExtend",
"priority": 5,
"method": "set_config"
}
}
}
}
プラグインのPHPファイルには以下のようなコードを記述します。
class SingleSignOnExtend extends PTPlugin {
public function __construct () {
parent::__construct();
}
private function load_provider( string $key ) {
$provider = $app->db->model( 'openid_provider' )->get_by_key( ['name' => 'google' ] );
if ( $provider->id ) {
return $provider;
}
$app->error('Provider %s is not found.' [$key]);
exit;
}
public function set_config( PTOIDC $app ) {
if ( $app->param( 'provider' ) === 'google' ) {
$provider = $this->load_provider( 'google' )
$app->client_id = $provider->client_id;
$app->client_secret = $provider->client_secret;
$app->oidc_config_url = $provider->oidc_config_url;
$app->oidc_config = $provider->oidc_config;
$app->rp_acs_endpoint_url = $provider->rp_acs_endpoint_url;
}
}
}
また、ログイン関連の処理について独自の実装を書くことができるようなフックポイントsinglesignon_pre_login.saml
・singlesignon_pre_login.oidc
を用意しています。シングルサインオンプラグインはPowerCMS Xにユーザー・メンバーの情報が予め登録されている前提になっていますが(さまざまな要件がありそうで最良の実装をまだ想像しきれませんでした)、フックポイントを利用することでSAML Response・JWTで受け取ったデータを基にユーザーを新規登録したり、IDプロバイダ毎に内容が違うであろうSAML Responseへの対応ができたりするでしょう。
まとめ
PHPやPowerCMS Xプラグインの作成を深く理解することで、より良いプラグインに仕上げることができることを実感できました。これからも研究を重ね、よりよりサービスが提供できるように努めてまいります。