大口顧客からは遅かれ早かれこう聞かれます。自社のスタッフが会社のアカウントでこのアプリケーションにサインインできますか、と。彼らが求めているのは、また別のユーザー名とパスワードではありません。既に他のすべてで使っている Microsoft Entra ID、Okta、AD FS のログインであり、そこには自社のパスワードポリシー、自社の二要素認証、そして誰かが退職した当日にアカウントを止められる一元管理が含まれます。
顧客の ID 管理チームが期待する答えは SAML 2.0 です。新しいログインコンポーネントの概要では、SAML はわずか一段落しか扱いませんでした。この記事ではフロー全体を扱います。TsgcSAMLServiceProvider が何をするか、ログインページと Assertion Consumer Service のコード、主要な identity provider にアプリケーションを登録する方法、そしてどこにもアカウントを持たずに今日これらすべてをテストする方法です。
SAML サインインの仕組み
登場するのは三者です。あなたのアプリケーションは service provider(SP)です。顧客のディレクトリは identity provider(IdP)です。ブラウザが両者間でメッセージを運ぶため、あなたのサーバーと IdP が直接やり取りすることはありません。
- ユーザーがログイン URL を開きます。アプリケーションは AuthnRequest を構築し、ブラウザを IdP へリダイレクトします。
- IdP は、会社が定めたパスワード、MFA、条件付きアクセスのルールに従ってユーザーをサインインさせます。
- IdP は署名済みの SAMLResponse で応答し、ブラウザはそれをあなたの Assertion Consumer Service(ACS)URL へ POST します。
- アプリケーションはレスポンスを検証し、その中で指定されたユーザーのために独自のセッションを作成します。
ステップ 4 こそが SAML 実装が誤りやすい箇所であり、まさにこのコンポーネントが代わりに処理してくれる部分です。
Service Provider をステップごとに見る
TsgcSAMLServiceProvider は HTTP サーバーではありません。SAML メッセージの構築とチェックを行うもので、アプリケーションが既に持っているサーバー、たとえば TsgcWebSocketHTTPServer や TsgcHTTPServer のリクエストハンドラーから呼び出します。
- アプリケーションを記述します。 アプリケーションの一意な名前(通常はメタデータの URL)である
EntityIDと、レスポンスが届く https URL であるAssertionConsumerServiceURLを設定します。 - identity provider を記述します。 IdP のメタデータ文書を指定して
LoadIdPMetadataを呼び出します。これにより IdP の entity ID、サインイン URL と binding、すべての署名証明書が読み込まれます。メタデータがない場合は、IdPEntityID、IdPSSOURL、IdPCertificatesを手動で設定します。 - リクエストを送信します。
GetAuthnRequestRedirectURLは、ブラウザをリダイレクトすべき URL を返します。HTTP-POST binding しか提供しない IdP の場合、GetAuthnRequestPostFormが代わりにリクエストを POST するページを返します。 - リクエスト id を保持します。 どちらのメソッドも新しい AuthnRequest の id を返します。これをランダムな RelayState かセッションクッキーをキーにしてサーバー上に保存し、応答が届いたら削除することで、各リクエストが一度だけ応答されるようにします。
- レスポンスを処理します。 ACS の URL で、POST された SAMLResponse、RelayState、保存しておいたリクエスト id を指定して
ProcessResponseを呼び出します。Trueが返れば、TsgcSAMLResultにNameID、SessionIndex、IdP から送られたすべての属性が格納されます。Falseが返れば、ErrorMessageに理由が示され、OnSAMLErrorが発生します。
起動時に一度 EntityID、AssertionConsumerServiceURL、LoadIdPMetadata を設定しておけば、ログインページと ACS は一つのリクエストハンドラーに収まります。
uses
sgcAuth_SAML_SP;
procedure TMyApp.OnCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
vRelayState, vRequestID: string;
oResult: TsgcSAMLResult;
begin
if ARequestInfo.Document = '/saml/login' then
begin
// 1. send the browser to the identity provider
vRelayState := NewRelayState;
AResponseInfo.Redirect(FSAML.GetAuthnRequestRedirectURL(vRelayState,
vRequestID));
// 2. keep the request id, the response must answer it
AddPendingRequest(vRelayState, vRequestID);
end
else if (ARequestInfo.Document = '/saml/acs') and
SameText(ARequestInfo.Command, 'POST') then
begin
// 3. the browser posts SAMLResponse and RelayState back
vRelayState := ARequestInfo.Params.Values['RelayState'];
vRequestID := TakePendingRequest(vRelayState);
oResult := TsgcSAMLResult.Create;
try
if FSAML.ProcessResponse(ARequestInfo.Params.Values['SAMLResponse'],
vRelayState, vRequestID, oResult) then
begin
// 4. signed in: create your own session for this user
CreateUserSession(AResponseInfo, oResult.NameID, oResult.SessionIndex);
AResponseInfo.Redirect('/');
end
else
AResponseInfo.ResponseNo := 403; // log oResult.ErrorMessage
finally
oResult.Free;
end;
end;
end;
NewRelayState、AddPendingRequest、TakePendingRequest、CreateUserSession は、あなた自身が実装するコードを表しています。すなわち GUID、RelayState をキーとして各リクエスト id を一度だけ払い出す thread safe なリスト、そしてアプリケーションのセッションクッキーです。属性は Name=Value 形式の行として届くため、oResult.Attributes.Values['email'] のように名前で読み取れます。Entra ID は http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress のような claim URI で属性名を付けます。
RelayState は IdP の署名の対象には含まれません。これは自分の保留中リクエストを見つけるためのキーとしてのみ使用し、確認せずにリダイレクトする URL としては決して使わないでください。
Identity Provider にアプリケーションを登録する
GetMetadata は service provider のメタデータを返します。つまり entity ID と、HTTP-POST binding を用いた ACS URL です。これを /saml/metadata のような URL で公開するか、ファイルに保存して IdP に渡します。どの identity provider も SP の entity ID と ACS URL という同じ二つの値を求めるため、以下の説明は主に各コンソールがそれらをどこで保持しているかについてのものです。いずれの場合も、アサーションの暗号化は無効のままにしてください。
- Microsoft Entra ID。 Enterprise applications、New application、Create your own application (non-gallery)。Single sign-on で SAML を選び、SP のメタデータをアップロードするか、Identifier (Entity ID) と Reply URL を入力します。ユーザーやグループを割り当て、SAML Certificates に表示される App Federation Metadata Url を読み込みます。
- Okta。 Applications、Create App Integration、SAML 2.0。Single sign-on URL はあなたの ACS URL で、“Use this for Recipient URL and Destination URL” にチェックを入れ、Audience URI はあなたの entity ID です。email、firstName、lastName などの attribute statements を追加し、ユーザーやグループを割り当て、Sign On タブから Metadata URL を読み込みます。
- AD FS。 claims aware Relying Party Trust を追加し、SP のメタデータをインポートします。AD FS は https エンドポイントのみを受け付けます。Name ID を送る claim rules を追加します。たとえば E-Mail-Addresses を E-Mail Address として送信し、続けて E-Mail Address を Name ID に変換します。IdP のメタデータは
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xmlにあります。 - Google Workspace。 Admin console、Apps、Web and mobile apps、Add custom SAML app。IdP のメタデータをダウンロードし、ACS URL と entity ID を入力し、Name ID(たとえば主要なメールアドレス)を選び、ユーザーに対してアプリを有効化します。
- Keycloak。 Client ID があなたの entity ID である SAML client を作成するか、SP のメタデータをインポートします。Keycloak は既定で文書全体に署名するため、Sign assertions も併せて有効にします。Client signature required が有効な場合は、
SignAuthnRequests、SPCertificate、SPPrivateKeyを設定します。IdP のメタデータはhttps://<host>/realms/<realm>/protocol/saml/descriptorにあります。
どの IdP であっても最後のステップは同じです。そのメタデータを LoadIdPMetadata に渡します。文書が複数のエンティティを記述している場合、第二引数で自分のものを選択します。
ProcessResponse が検証すること
SAML レスポンスは署名済みの XML 文書であり、よく知られた SAML の脆弱性の多くは、service provider に署名された内容とは異なるものを読ませる手口です。レスポンスは、次のすべての検証に合格した場合にのみ受理されます。
- IdP 証明書に対する署名検証。 レスポンスは
IdPCertificatesにある証明書だけで検証されます。メッセージに埋め込まれた証明書が信頼されることは決してありません。攻撃者も同様に証明書を埋め込めるためです。既定値であるWantAssertionsSignedを用いると、アサーション自体も自分の署名を持つ必要があります。 - signature wrapping への対策。 署名は、文書内で ID が一意な要素を参照していなければならず、検証後は署名された要素だけが読み取られます。署名済みのアサーションの横に紛れ込ませた署名なしのアサーションは、決して参照されません。
- 単一のアサーション。 レスポンスには、response 直下にちょうど一つのアサーションが含まれていなければなりません。
- audience と recipient。 audience はあなたの
EntityID、recipient はあなたのAssertionConsumerServiceURLでなければならず、これにより他のアプリケーション向けに発行されたアサーションは拒否されます。 - 時間窓。 NotBefore と NotOnOrAfter は、既定で 2 分である
ClockSkew秒の許容誤差をもって UTC と照合されます。MaxAssertionAgeによってアサーションの最大経過時間を制限することもできます。 - InResponseTo。 レスポンスは、あなたが保存したリクエスト id に応答するものでなければなりません。
AllowIdPInitiatedを設定しない限り、要求されていない、IdP 主導のレスポンスは拒否されます。 - リプレイキャッシュ。 受理された各アサーションの ID は失効するまで保持されるため、同じレスポンスが二度 POST されると拒否されます。このキャッシュは thread safe で、メモリ上に存在します。複数のサーバーでサインインを共有する場合は、
DoAddToReplayCacheをオーバーライドして ID を共有ストアに保持してください。 - SHA-1 は既定で無効。 RSA-SHA1 署名と SHA-1 ダイジェストは、まだそれらを必要とする IdP 向けに
AllowSHA1を設定しない限り拒否されます。
パーサーは DOCTYPE 宣言も拒否するため外部エンティティは存在せず、文書のサイズとネストの深さも制限します。issuer は設定した IdP でなければなりません。最初に失敗した検証で検証処理は停止し、その理由は ErrorMessage に記録されます。これをログに残し、ユーザーにはシンプルな「サインインに失敗しました」というページを表示してください。
アカウントなしで試す
SAML の動作を確認するのに Entra ID のテナントは不要です。Mock SAML は mocksaml.com にある無料のテスト用 identity provider です。任意の service provider を受け入れ、AuthnRequest から audience と ACS URL をそのまま取得するため、登録するものは何もありません。
デモ Demos\26.Authentication\03.SAML_ServiceProvider は TsgcWebSocketHTTPServer 上に構築された完全な service provider で、http://localhost:8090 上に /login、/acs、/metadata の各エンドポイントを提供します。
- デモをビルドし、libcrypto-3.dll と libssl-3.dll を実行ファイルの隣に置いてください。これらはデモのフォルダーにあり、OpenSSL が RSA 署名の検証に使用します。
- Load IdP metadata をクリックします。既定のソースは mocksaml.com のメタデータ URL です。
- Start をクリックし、続けて Open Browser をクリックして、サインインリンクをたどります。
- mocksaml.com で、example.com ドメインの任意のユーザー名と任意のパスワードを入力します。
- ブラウザが ACS に戻り、ページに NameID、SessionIndex、そして id、email、firstName、lastName の各属性が表示されます。
これが動作したら、http://localhost:8090/metadata を開いて実際の IdP に登録し、デモで IdP のメタデータを読み込んで、もう一度サインインしてみてください。AD FS の場合、AD FS は https しか受け付けないため、まず SSL 有効の状態でデモを実行してください。
現在の制限
- 暗号化されたアサーションには対応していません。 EncryptedAssertion や暗号化された NameID を含むレスポンスは拒否されます。IdP 側ではアサーションの暗号化を無効のままにしてください。アサーションは引き続き署名され、https 経由で送信されます。
- Single Logout には対応していません。 SLO は実装されていません。アプリケーションが自身のセッションを終了し、独自のログアウトを構築できるように
SessionIndexが返されます。
ドキュメント
入手方法
TsgcSAMLServiceProvider は、Delphi および C++ Builder 向け sgcWebSockets の Enterprise エディションと All-Access エディションに含まれており、同じコンポーネントは sgcWebSockets .NET の一部でもあります。認証機能だけが必要な場合は、sgcAuth パックに、他のログインコンポーネントとともに含まれています。ユニット名は sgcAuth_SAML_SP で、コンポーネントをフォームに配置するまで、既存のアプリケーションには何の変化もありません。
次に読む
動画で見る
eSeGeCe チャンネルに “SAML single sign-on in Delphi with Entra ID, Okta and AD FS” という短い動画があります。IDE 上のコードと、mocksaml.com を相手にしたデモでのライブサインインの様子を紹介しています。
ご質問、ご意見、あるいは identity provider との接続にお困りですか。お問い合わせください。このコードを書いた本人から返信いたします。
