SAML クレーム、SharePoint、WCF、Windows Token Service へのクレーム、および制約付き委任を使用して SQL Server にアクセスする
原文の記事の投稿日: 2011 年 8 月 7 日 (日曜日)
さて、今回の投稿は、これまでの中で最もタイトルが長くなりそうですが、それは今回の説明に関係するすべてのテクノロジを載せようと思うからです。今回説明する分野については喧々諤々とさまざまな声が上がっているようですが、話をまとめると要するに、SAML クレーム ユーザーを利用して、Windows コンテキストを取得して他のアプリケーションにアクセスするにはどのような方法があるかということです。SharePoint 2010 は、Windows Token Service へのクレーム (以下、c2wts と表記します) の使用を制限付きでサポートしていますが、それは少数のサービス アプリケーションしか持たない Windows クレーム ユーザーに限られています。だれもが疑問に思います。SharePoint 2010 で有効な UPN クレームを持つ SAML クレーム ユーザーを使用できないのはなぜでしょうか。実際、それを技術的に不可能とする理由はありません。となれば、つまりこういうことです。各種認証の制限事項と、各種認証を使用するサービス アプリケーションの制限事項の間で、SAML ユーザーを基本 Windows アカウントとして他のアプリケーションに接続する方法を構築する必要があります。この方法の基本事項を理解するうえで、この投稿をぜひお役立てください。
このシナリオでは基本的なアプローチとして、他のアプリケーションのデータに対するエンド ユーザーからの要求をすべて処理する WCF サービス アプリケーション (ここでは SQL Server) を作成します。そこで、SharePoint サイトにアクセスする SAML ユーザーを利用し、SQL Server からデータを取得するときに、要求をその SAML ユーザーの Windows アカウントとして作成します。メモ: この記事は SAML クレーム ユーザーに関するものですが、これとまったく同じ方法を Windows クレーム ユーザーでも使用できます。Windows クレーム ユーザーは、ログイン時に既定で UPN クレームを取得します。次の図に、プロセスの全体像を示します。
SQL Server を構成する
SQL Server 側の作業から始めます。このシナリオでは、SQL Server は "SQL2" という名前のサーバーで実行されます。SQL サービス自体はネットワーク サービスとして実行されます。つまり、SQL サービス用の SPN を作成する必要はないということです。SQL サービスがドメイン アカウントとして実行される場合は、そのサービス アカウント用の SPN を MSSQLSvc のために作成する必要があります。この特定のシナリオでは、以前の Northwinds データベースを使用してデータを取得します。要求を実行するユーザーの ID についての説明が簡単になるように、Ten Most Expensive Products ストアド プロシージャを次のように変更しました。
CREATE procedure [dbo].[TenProductsAndUser] AS
SET ROWCOUNT 10
SELECT Products.ProductName AS TenMostExpensiveProducts, Products.UnitPrice, SYSTEM_USER As CurrentUser
FROM Products
ORDER BY Products.UnitPrice DESC
ここで特に注目してほしいのは、SYSTEM_USER を SELECT 文に追加した点です。これは列内の現在のユーザーを返します。つまり、クエリを実行して結果を取得すると、現在のユーザー名を含む列がグリッドに表示されるため、クエリが現在のユーザーの ID として実行されたかどうかを即座に確認できるというわけです。この特定のシナリオでは、このストアド プロシージャを実行する 3 つの Windows ユーザー権限を付与しており、それ以外のユーザーがこのストアド プロシージャを実行することはできません (最終出力を見るとよくわかります)。
WCF サービス アプリケーションを作成する
次に、SQL からデータを取得する WCF サービス アプリケーションを作成します。ここでは、以前、CASI キットに関する投稿のパート 2 (https://blogs.msdn.com/b/sharepoint_jp/archive/2010/12/16/azure-sharepoint-2.aspx) で説明したガイドラインに従いました。このガイドラインの指示を実行して SharePoint ファームと WCF アプリケーションの間に信頼関係を構築しました。要求を実行するユーザーのクレームを取得するには信頼関係が必要です。だれしも UPN クレーム値をパラメーターとして渡したくないでしょう。そうでないと、第三者が別の UPN クレーム値を渡すだけで他のユーザーの ID をスプーフィングできてしまいます。WCF と SharePoint の間に信頼関係を正しく構築できたら、次は、以下の処理を実行するメソッドを記述します。
- UPN クレームを取得する。
- c2wts を使用してユーザーを偽装する。
- 偽装されたユーザーとして SQL からデータを取得する。
これらの処理の実行に使用したコードを次に示します。
//the following added for this code sample:
using Microsoft.IdentityModel;
using Microsoft.IdentityModel.Claims;
using System.Data;
using System.Data.SqlClient;
using System.Security.Principal;
using Microsoft.IdentityModel.WindowsTokenService;
using System.ServiceModel.Security;
public DataSet GetProducts()
{
DataSet ds = null;
try
{
string conStr = "Data Source=SQL2;Initial Catalog=
Northwind;Integrated Security=True;";
//ask for the current claims identity
IClaimsIdentity ci =
System.Threading.Thread.CurrentPrincipal.Identity as IClaimsIdentity;
//make sure the request had a claims identity attached to it
if (ci != null)
{
//see if there are claims present before running through this
if (ci.Claims.Count > 0)
{
//look for the UPN claim
var eClaim = from Microsoft.IdentityModel.Claims.Claim c in ci.Claims
where c.ClaimType == System.IdentityModel.Claims.ClaimTypes.Upn
select c;
//if we got a match, then get the value for login
if (eClaim.Count() > 0)
{
//get the upn claim value
string upn = eClaim.First().Value;
//create the WindowsIdentity for impersonation
WindowsIdentity wid = null;
try
{
wid = S4UClient.UpnLogon(upn);
}
catch (SecurityAccessDeniedException adEx)
{
Debug.WriteLine("Could not map the upn claim to " +
"a valid windows identity: " + adEx.Message);
}
//see if we were able to successfully login
if (wid != null)
{
using (WindowsImpersonationContext ctx = wid.Impersonate())
{
//request the data from SQL Server
using (SqlConnection cn = new SqlConnection(conStr))
{
ds = new DataSet();
SqlDataAdapter da =
new SqlDataAdapter("TenProductsAndUser", cn);
da.SelectCommand.CommandType =
CommandType.StoredProcedure;
da.Fill(ds);
}
}
}
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
return ds;
}
最後に、決して複雑なコードではないので、実際の処理について簡単に説明します。まず、有効なクレーム ID コンテキストがあるかどうかを確認し、ある場合は、クレームのリストをクエリして UPN クレームを探します。ここでは UPN クレームが見つかったとして、その UPN クレームから値を抽出し、c2wts の呼び出しを行って S4U ログインをそのユーザーとして実行します。ログインが正常に実行されると、WindowsIdentity が返されます。次に、この WindowsIdentity を利用して偽装コンテキストを作成します。ユーザーを偽装したら、SQL Server への接続を作成してデータを取得します。トラブルシューティングに関するいくつかの簡単なヒントを次に示します。
- アプリケーション プールに対して c2wts の使用を許可するように c2wts を構成していない場合、外部の catch ブロックで "WTS0003: 呼び出し元はサービスへのアクセスが許可されていません (The caller is not authorized to access the service.)" などのエラーが発生します。c2wts の詳細と c2wts の構成に関するリンクを以下に示します。
- Kerberos の制約付き委任が正しく設定されていない場合に、da.Fill(ds); コード行でストアド プロシージャを実行しようとすると、匿名ユーザーにはこのストアド プロシージャを実行する権限がないことを示す例外がスローされます。このシナリオに合わせて制約付き委任を構成するいくつかのヒントを以下に示します。
c2wts を構成する
c2wts は既定で、a) 手動で起動する、b) どのユーザーに対しても使用を許可しないように構成されます。この構成を、a) 自動的に起動し、b) WCF サービス アプリケーション用のアプリケーション プールに対して c2wts の使用を許可するように変更しました。ここでは、この許可を与える方法について詳しく説明しません。代わりに、 https://msdn.microsoft.com/ja-jp/library/ee517258.aspx の記事を読むことをお勧めします (構成情報の記載は最後にあります)。この記事には必要な情報がすべて含まれています。また、c2wts の背景情報の詳細については、 https://msdn.microsoft.com/ja-jp/library/ee517278.aspx を読むことをお勧めします。
メモ: この最後の記事には、sc config c2wts depend=cryptosvc というコードを実行して c2wts の依存関係を作成することを推奨する記述がありますが、これは大きな誤りです。この指示には絶対に従わないでください。これには入力ミスがあり、"cryptosvc" は有効なサービス名ではありません。少なくとも Windows Server 2008 R2 では実行しないでください。これを実行すると、依存関係が削除用としてマークされている、あるいは依存関係が見つからないことが c2wts によって報告されるため、c2wts が今後起動しなくなります。私は実際にこうした状況に陥ってしまい、依存関係を iisadmin に変更しました (私のケースでは、少なくとも私的な目的で WCF ホストを実行して c2wts を使用できればよかったので、このように変更しました)。そうでなければ、そこで行き詰ったでしょう。
Kerberos の制約付き委任を構成する
それでは、読者の皆様がこの記事を読んでお疲れになっても困るので、次のことを述べておきます。
- ここでは、Kerberos の制約付き委任を実行するうえでの重要事項について詳しく説明しません。そうした内容は他の書物に書かれています。
- 信憑性には疑問が残りますが、このパーツは、これを記述したときにはとてもスムーズに機能しました。
それでは、委任のために必要な事項について詳しく説明します。まず、前述のとおり、SQL Server サービスはネットワーク サービスとして実行されるため、ここで何か操作を行う必要はありません。また、WCF アプリケーション プールは vbtoys\portal という名前のドメイン アカウントとして実行されます。そこで、このドメイン アカウントのために次の 2 つの操作を行う必要があります。
- ドメイン アカウント用の HTTP SPN を作成します。この SPN には、委任元サーバーの NetBIOS 名と完全修飾名の両方を使用します。このケースでは、WCF サーバーの名前は AZ1 なので、次のような 2 つの SPN を作成します。
- setspn -A HTTP/az1 vbtoys\portal
- setspn -A HTTP/az1.vbtoys.com vbtoys\portal
- "SQL2" サーバー上で実行される SQL Server サービスへの Kerberos の制約付き委任に対する信頼関係をアカウントに構成する必要があります。そのために、ドメイン コントローラーに移動し、[Active Directory ユーザーとコンピューター] (Active Directory Users and Computers) を開きます。vbtoys\portal ユーザーをダブルクリックし、[委任] (Delegation) タブをクリックして、この信頼関係を構成します。特定のサービスへの委任に対する信頼関係のみを設定し、認証プロトコルには任意の種類の認証プロトコルを使用しました。委任構成の設定を示す図へのリンクを次に示します。
次に、制約付き委任に対する信頼関係を WCF アプリケーション サーバーに構成する必要があります。幸いなことに、操作手順は、前述のユーザーの場合とまったく同じです。つまり、[Active Directory ユーザーとコンピューター] (Active Directory Users and Computers) でコンピューター アカウントを見つけて、そこで信頼関係を構成するだけです。委任構成の設定を示す図へのリンクを次に示します。
さらに続けて、SharePoint 以外のスタッフをすべて設定および構成したら準備完了です。最後に Web パーツのテストが必要です。
SharePoint Web パーツを作成する
Web パーツを作成するのはとても簡単です。前述のパターンに従って、SharePoint への WCF 呼び出しを作成して現在のユーザーの ID を渡すだけです (https://blogs.technet.com/b/speschka/archive/2010/09/08/calling-a-claims-aware-wcf-service-from-a-sharepoint-2010-claims-site.aspx (英語))。また、CASI キットを使用して接続を作成し、WCF を呼び出すこともできましたが、あえて手動で行うことにしました (その方が図示しやすいため)。Web パーツの基本的な作成手順は次のとおりです。
- 新しい SharePoint 2010 プロジェクトを Visual Studio 2010 で作成します。
- WCF サービス アプリケーションへのサービス参照を作成します。
- 新しい Web パーツを追加します。
- Web パーツにコードを追加して、WCF からデータを取得してグリッドに表示します。
- Visual Studio プロジェクトで生成された app.config に含まれるすべての情報を、Web パーツをホストする Web アプリケーションの web.config ファイルの <system.ServiceModel> セクションに追加します。
メモ: app.config ファイルには decompressionEnabled という名前の属性があります。app.config ファイルの情報を web.config ファイルに追加する前に、この属性を必ず削除してください。この属性が Web パーツに残っていると、サービス参照プロキシのインスタンスの作成時に、Web パーツからエラーがスローされます。
上記の手順を見てください。どの手順も非常にわかりやすいものです (4 番目の手順を除く)。したがって、これらの手順について詳しく説明することは省略し、Web パーツのコードのみを次に示します。
private DataGrid dataGrd = null;
private Label statusLbl = null;
protected override void CreateChildControls()
{
try
{
//create the connection to the WCF and try retrieving the data
SqlDataSvc.SqlDataClient sqlDC = new SqlDataSvc.SqlDataClient();
//configure the channel so we can call it with FederatedClientCredentials
SPChannelFactoryOperations.ConfigureCredentials<SqlDataSvc.ISqlData>(
sqlDC.ChannelFactory, Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);
//create the endpoint to connect to
EndpointAddress svcEndPt =
new EndpointAddress("https://az1.vbtoys.com/ClaimsToSqlWCF/SqlData.svc");
//create a channel to the WCF endpoint using the
//token and claims of the current user
SqlDataSvc.ISqlData sqlData =
SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser
<SqlDataSvc.ISqlData>(sqlDC.ChannelFactory, svcEndPt);
//request the data
DataSet ds = sqlData.GetProducts();
if ((ds == null) || (ds.Tables.Count == 0))
{
statusLbl = new Label();
statusLbl.Text = "No data was returned at " + DateTime.Now.ToString();
statusLbl.ForeColor = System.Drawing.Color.Red;
this.Controls.Add(statusLbl);
}
else
{
dataGrd = new DataGrid();
dataGrd.AutoGenerateColumns = true;
dataGrd.DataSource = ds.Tables[0];
dataGrd.DataBind();
this.Controls.Add(dataGrd);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
繰り返しになりますが、これも一目で理解できるでしょう。最初の部分では、WCF サービスへの接続を作成し、現在のユーザーのクレームを渡しています。詳細については、上記のリンクを使用して、このトピックに関する以前のブログ投稿を参照してください。残りの部分では、データセットを取得し、データセットにデータが含まれる場合は、そのデータセットをグリッドに結合し、データセットが失敗した場合は、データがないことを示すラベルを表示します。これらのすべての部分の連携を次の 3 つのスクリーンショットに示します。最初の 2 つは、2 名の異なるユーザーのために機能する様子を示しています。ここでは CurrentUser 列の内容を確認できます。3 つ目は、ストアド プロシージャを実行する権限が付与されていないユーザーについてのものです。
最後になりますが、WCF サービス アプリケーションと Web パーツのコードをこの投稿に添付しました。また、これらの投稿の書式設定が単調で評判が悪いため、今回の投稿を記述したオリジナルの Word 文書も添付しています。
これはローカライズされたブログ投稿です。原文の記事は、「Using SAML Claims, SharePoint, WCF, Claims to Windows Token Service and Constrained Delegation to Access SQL Server」をご覧ください。