AEON Advent Calendar 2023 17日目の記事です。
社内でWebアプリを開発する際、共通のIDでシステムにログインできるように、ADFS認証(SAML認証)を実装するのですが、
社内で共有されている実装例の言語が限定されていたり、PHP/Laravelで実装しようとした時に、インターネット上に古い情報しかなかったりしたので、
私のこれまでの経験をベースに、ADFS認証(SAML認証)についてわかったことと、PHP/Laravelでの実装例をご紹介できればと思います。
ADFSとは
Active Directory Federation Servicesの略で、Active Directoryのリソースを使って認証する際に、
SSO(Single Sign-On)、つまり様々なシステムに対して、共通のIDでシステムにログインできるようにするためのActive Directoryの機能です。
ADFSでは、様々な認証方法を利用することができ、WS-Federation、SAML、OpenID Connectなどありますが、本記事では、主にSAMLについて説明します。
SAMLとは
Security Assertion Markup Languageの略称で、ユーザー認証をするためのXMLベースの認証方法の規格です。
SAMLでは、サービスプロバイダ(SP)とアイデンティティプロバイダ(IdP)間のSAML Request・SAML Responseのやりとりによって、認証を実現しています。
ここでいう、IdPとは認証情報を保管するActive Directory(ADFS)で、SPとはIdPから認証情報を受けてログインなどをしたいシステムになります。
特徴としては、IdPとSP間は直接通信せず、全てのやり取りはユーザーのクライアント上で行われることが挙げられます。
IdP(Identity Provider)とは
IdPは、ユーザの認証情報を管理し、SPからの要求に応じてユーザの認証情報を返すものですが、
ユーザーがログインを実行する前にSPのMetadataとIdPのMetadataを交換しておいてお互いに「信頼関係」を結んでおく必要があります。
IdPのMetadataには下記情報が必要になります。
- Entity ID:IdPで信頼関係を結んでいるSPの中で一意のID
- SSO URL:SPからIdPにリダイレクトする際に、SAMLRequestを載せてリダイレクトする先のURL
また、IdPのMetadataにはSAML Response内にユーザの認証情報が格納されている「Attribute」の構造が記載されており、
SPの機能によってはIdPのMetadataを読み込むことでAttributeを自動的にマッピングして、
プログラム内で扱いやすい配列の形に変換してくれる機能があるものもあります。
SP(Service Provider)とは
SPは、IdPに認証情報のリクエストを送り、
IdPから受け取った認証情報を使ってユーザーに機能を提供するシステムになります。
(SAML認証を実装したいシステム・被認証システム)
SPには下記機能が必要となります。
- Metadataの生成
- SAML Responseの解析
- (任意)IdPのMetadata解析機能
また、SPのMetadataには、下記情報が必要になります。
- Entity ID:IdPで信頼関係を結んでいるSPの中で一意のID
- AssertionConsumerServiceURL:IdPからSPにリダイレクトする際に、SAMLResponseを載せてリダイレクトする先のURL
- (任意)証明書のX509文字列:後述
SPで作成するMetadataは、IdPで「リクエストしてきたSPが認証情報を渡すに値するか判断」するための情報です。
しかし、IdPでは最悪Entity IDとAssertionConsumerServiceURLさえ合っていれば認証情報を返せてしまいます。
そのことから、IdPによって「Metadataに証明書情報が必須」としている場合があります。
ここで設定している証明書はサーバ証明書などを利用することも可能ですが、サーバ証明書の期限ごとにIdPと信頼関係を結び直さなければならなかったり、「SPが正しいSPであること」を証明できれば良いので、
長めに期限を設定した自己証明書などを設定するのが良いと思われます。
ADFS認証(SAML)の実装
さて、仕様についてお話ししたところで実装を、となるのですが、
IdPのSAML Responseの解析・SPのMetadataの生成・IdP、SP間のリクエストのやり取りなど、
1から実装するのは骨が折れますので、世の中にあるパッケージを利用しよう、となります。
私が対応したプロジェクトでは下記のようなパッケージを候補として選定し、
最終的にSocielite(SAML2 Service Provider)を採用いたしました。
選定理由は下記の2点です。
- Laravel公式パッケージであるため、サポート切れの危険性が少ない
- 「プロバイダ」という仕組みにより、大きな変更を加えずにSAML以外にもAzure AD認証などの実装もできることから
将来ADFS以外の認証方法が必要となったときに変更しやすい
パッケージ | Shibboleth SP | Socielite (SAML2 Service Provider) |
aacotroneo/laravel-saml2 | 24Slides/laravel-saml2 |
---|---|---|---|---|
言語 | Java | PHP/Laravel | PHP/Laravel | PHP/Laravel |
サポート |
3.4.1.4 最終更新:2023/10/11 |
最終更新:2023/11/8 | 最終更新:2019/10/2 | 最終更新:2023/9/28 |
特徴 | Laravelの公式パッケージ (の拡張プロバイダ) |
SPの情報などをDB上に持つ |
※Laravel開発での移設プロジェクトでの選択肢ですが、移設前のプロジェクトがJavaのプロジェクトであったため、Shibboleth SPが選択肢にあります。
以下に各パッケージを用いたSAML認証の実装方法を示します。
なお、下記前提条件のもと説明します。
- PHP、Composerをインストール済み
- Laravelプロジェクトを作成済み
- IdPはSAMLTest.idを利用
Socialiteの実装方法
Socialiteの実装方法
1.Socialiteをインストール
composer require laravel/socialite
2.Socialite SAML2 Service Providerをインストール
composer require socialiteproviders/saml2
3.app.phpの設定
Socialiteで追加プロバイダを使えるようにするために、
config/app.phpに設定を追加します。
・・・
'providers' => ServiceProvider::defaultProviders()->merge([
// ・・・
\SocialiteProviders\Manager\ServiceProvider::class,
])->toArray(),
・・・
4.EventServiceProviderの設定
2.Socialite SAML2 Service ProviderをインストールでインストールしたパッケージをSocialiteから呼び出せるようにするために、
app/providers/EventServiceProvider.phpに設定を追加します。
・・・
protected $listen = [
・・・
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Saml2\Saml2ExtendSocialite::class.'@handle',
],
];
・・・
5.web.phpの設定①
web.phpに、SAMLRequestをIdPに送るときのトリガーとなるURLを設定します。
※Route::getメソッドの第一引数の値は任意の値で構いません。今回は、/auth/redirect
とします。
・・・
Route::get('/auth/redirect', function () {
return Socialite::driver('saml2')->redirect();
});
・・・
web.phpに、SPのMetadataを出力するURLを設定します。
※Route::getメソッドの第一引数の値は任意の値で構いません。今回は、/auth/saml2/metadata
とします。
・・・
Route::get('/auth/saml2/metadata', function () {
return Socialite::driver('saml2')->getServiceProviderMetadata();
});
・・・
6.Socialite SAML2 Service Providerの設定
IdP Metadataの情報と、SPのAssertionConsumerServiceURLの情報をconfig/services.phpに記載します。
今回、metadata
はSAMLtest.idのDownload Metadataに記載の情報を指定します。
・・・
'saml2' => [
'metadata' => 'https://idp.co/metadata/xml',
],
・・・
なお、ここで様々な設定をすることで、SPのMetatdataに記載する内容を変更し、IdPでログアウトした時のリダイレクト先
などを指定することもできます。
その他に設定可能な内容は下記ページをご確認ください。
https://socialiteproviders.com/Saml2/
・・・
'saml2' => [
'metadata' => 'https://samltest.id/saml/idp', // IdPのMetadata
'sp_acs' => 'auth/callback', // IdPでログイン後、SAMLResponseを受け取るリダイレクト先
'sp_sls' => 'auth/saml2/logout', // IdPでログアウト後のリダイレクト先
'sp_certificate' => file_get_contents('path/to/sp_saml.crt'), // SPのMetadataに反映する証明書の情報
'sp_private_key' => file_get_contents('path/to/sp_saml.pem'), // 証明書の秘密鍵
'sp_tech_contact_surname' => 'Doe', // SPのMetadataに反映する担当者の名前
'sp_tech_contact_givenname' => 'John',// SPのMetadataに反映する担当者の姓
'sp_tech_contact_email' => 'john.doe@example.com',// SPのMetadataに反映する連絡先メールアドレス
'sp_org_lang' => 'en',// SPのMetadataに反映する会社の言語
'sp_org_name' => 'Example Corporation Ltd.',// SPのMetadataに反映する会社名
'sp_org_display_name' => 'Example Corporation',// SPのMetadataに反映する会社の表示名
'sp_org_url' => 'https://corp.example',// SPのMetadataに反映する会社のURL
],
・・・
6.SPのMetadataを取得する
5.web.phpの設定①で設定したSPのMetadataを出力するURLにアクセスし、Metadataを取得します。
Metadataの中身は下記のようになっており、6.Socialite SAML2 Service Providerの設定で設定した内容が反映されています。
<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="http://127.0.0.1:8000/auth/saml2" ID="_a40cc3c3cbeee2e3a914694cff0dc0abe9ed95db5f">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"
WantAssertionsSigned="true">
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<AssertionConsumerService index="0" isDefault="true"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://127.0.0.1:8000/auth/callback" />
</SPSSODescriptor>
</EntityDescriptor>
7.Metadataを交換してIdPと信頼関係を結ぶ
7.SPのMetadataを取得するで取得したSPのMetadataをIdPに登録して、信頼関係を結びます。
SAMLTest.idの場合は、こちらからXML形式のファイルをアップロードすることで信頼関係を結べます。
8.web.phpの設定②
web.phpに、IdPからSAMLResponseを受け取るためのリダイレクト先を設定します。
第2引数に処理を記載することで、受け取ったSAMLResponseをシステムの中で利用することができます。
今回は、SAMLResponseを正常に受信できたことを確認するため、SAMLResponseのデータをダンプする処理を追加しています。
※Route::getメソッドの第一引数の値は任意の値で構いません。今回は、/auth/callback
とします。
/auth/callback
を変更する場合、ここと、config/services.php
のsp_acs
の値を併せて設定してください。
※web.phpではなく、Controllerや、Middlewareに記述しても構いません。
・・・
Route::get('/auth/callback', function () {
$user = Socialite::driver('saml2')->user();
dd($user); //SAMLResponseのデータをダンプする処理
});
・・・
IdP側の設定によりSAMLResponseを受け取るためのリダイレクト先がPOSTリクエストを受け取れる形でないとならない場合、
Routeファサードのメソッドをpostにした上でconfig/services.php
に設定を追加する必要があります。
※これを実施した後はmetadataの内容が変わるため信頼関係を結び直してください。
・・・
Route::post('/auth/callback', function () {
$user = Socialite::driver('saml2')->user();
});
・・・
・・・
'saml2' => [
//・・・
'sp_default_binding_method' => \LightSaml\SamlConstants::BINDING_SAML2_HTTP_POST,
],
・・・
9.IdPへSAMLRequestを送り、IdPでログインする。
5.web.phpの設定①で設定した、「SAMLRequestをIdPに送るときのトリガーとなるURL」にアクセスします。
すると、IdPのMetadataの内容に基づいてリダイレクトし、IdPのログイン画面が表示されます。
ここで、URLに付加されたGetパラメータを確認すると、
「SAMLRequest」と、「Relaystate」という二つのパラメータがあることがわかります。
ここに記載されてある「SAMLRequest」はエンコードすることができ、
エンコードすると下記のようなXMLが取り出せます。
<?xml version="1.0"?>
<AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="_67a0c0443478575cffd90f16181c21d61b26ce1ee1" Version="2.0" IssueInstant="2023-12-16T13:52:41Z" Destination="https://samltest.id/idp/profile/SAML2/Redirect/SSO" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" AssertionConsumerServiceURL="http://127.0.0.1:8000/auth/callback"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://127.0.0.1:8000/auth/saml2</saml:Issuer><NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/></AuthnRequest>
9.IdPへSAMLRequestを送り、IdPでログインする。
IdPでログイン後8.web.phpの設定②で設定したURLにリダイレクトし、
第2引数に指定した処理が実行されれば設定は完了です。
※419 Page Expired
となる場合
IdPから受け取ったリクエスト(SAMLResponse)で、CSRFの検証ができないためにエラーとなっています。
app/Http/Middleware/VerifyCsrfToken.php
に8.web.phpの設定②で設定したURLを指定することで解消します。
・・・
protected $except = [
//
// 'auth/callback',
];
・・・
※Laravel\Socialite\Two\InvalidStateException
となる場合
Socialiteではsessionに格納されているstateを見て、stateがNullとなっていた場合にこのエラーを出す仕様になっています。
8.web.phpの設定②で設定したルートの中の処理を変更することで対処できます。
・・・
Route::get('/auth/callback', function () {
$user = Socialite::driver('saml2')->stateless()->user();
});
・・・
開発に役立つツール
テストIdP
テストの際に有用なサービス・ツールになります。
ADFSを検証用に使えない場合に仮のIdPとして利用できます。
他に有用なサービスなどありましたら、ご教示ください。
- SAMLtest.id:SPの動作確認や、IdPの動作確認もできるオンラインサービス
- SimpleSAMLphp:ダウンロードして、ローカルで動かせるIdP
SAMLRequest・SAMLResponseのエンコード
GETパラメータを解析し、XMLを取り出すためのツールです。
最後に
SAML認証は、必ずユーザの操作を介して認証を行う「対話型認証」のための認証方法になります。
「コンテナ」「API」などの活用が必要な今、OAuthや、OpenID Connectなどの「非対話型認証」が必要になってくるものと思われます。
必要性がありSAML認証などの認証方法を選ばなければならない場合でも、先を見据えたパッケージ選定・実装方法ができると良いと思います。
閲覧いただきありがとうございました。