サポート トークンのサンプルでは、WS-Security を使用するメッセージにトークンを追加する方法を示します。 この例では、ユーザー名セキュリティ トークンに加えて X.509 バイナリ セキュリティ トークンを追加します。 トークンはクライアントからサービスに WS-Security メッセージ ヘッダーで渡され、メッセージの一部は X.509 セキュリティ トークンに関連付けられている秘密キーで署名され、X.509 証明書が受信側に所有されていることを証明します。 これは、送信者を認証または承認するためにメッセージに複数の要求を関連付ける必要がある場合に便利です。 このサービスは、要求/応答通信パターンを定義するコントラクトを実装します。
対象
このサンプルでは、次の例を示します。
クライアントがサービスに追加のセキュリティ トークンを渡す方法。
サーバーが追加のセキュリティ トークンに関連付けられている要求にアクセスする方法。
サーバーの X.509 証明書を使用して、メッセージの暗号化と署名に使用される対称キーを保護する方法。
注
このサンプルのセットアップ手順とビルド手順は、このトピックの最後にあります。
クライアントがユーザー名トークンを使用して認証し、X.509 セキュリティ トークンをサポートする
サービスは、 BindingHelper
クラスと EchoServiceHost
クラスを使用してプログラムによって作成された通信用の単一のエンドポイントを公開します。 エンドポイントは、アドレス、バインディング、およびコントラクトで構成されます。 バインドは、 SymmetricSecurityBindingElement
と HttpTransportBindingElement
を使用してカスタム バインドで構成されます。 このサンプルでは、SymmetricSecurityBindingElement
を設定して、サービス X.509 証明書を使用し、転送中に対称キーを保護するとともに、UserNameToken
をサポート用の X509SecurityToken
と共に WS-Security メッセージ ヘッダーに渡します。 対称キーは、メッセージ本文とユーザー名セキュリティ トークンを暗号化するために使用されます。 サポート トークンは、WS-Security メッセージ ヘッダーに追加のバイナリ セキュリティ トークンとして渡されます。 サポート トークンの信頼性は、サポート X.509 セキュリティ トークンに関連付けられている秘密キーを使用してメッセージの一部に署名することによって証明されます。
public static Binding CreateMultiFactorAuthenticationBinding()
{
HttpTransportBindingElement httpTransport = new HttpTransportBindingElement();
// the message security binding element will be configured to require 2 tokens:
// 1) A username-password encrypted with the service token
// 2) A client certificate used to sign the message
// Instantiate a binding element that will require the username/password token in the message (encrypted with the server cert)
SymmetricSecurityBindingElement messageSecurity = SecurityBindingElement.CreateUserNameForCertificateBindingElement();
// Create supporting token parameters for the client X509 certificate.
X509SecurityTokenParameters clientX509SupportingTokenParameters = new X509SecurityTokenParameters();
// Specify that the supporting token is passed in message send by the client to the service
clientX509SupportingTokenParameters.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
// Turn off derived keys
clientX509SupportingTokenParameters.RequireDerivedKeys = false;
// Augment the binding element to require the client's X509 certificate as an endorsing token in the message
messageSecurity.EndpointSupportingTokenParameters.Endorsing.Add(clientX509SupportingTokenParameters);
// Create a CustomBinding based on the constructed security binding element.
return new CustomBinding(messageSecurity, httpTransport);
}
この動作では、クライアント認証に使用するサービス資格情報と、サービス X.509 証明書に関する情報も指定します。 このサンプルでは、サービス X.509 証明書のサブジェクト名として CN=localhost
を使用します。
override protected void InitializeRuntime()
{
// Extract the ServiceCredentials behavior or create one.
ServiceCredentials serviceCredentials =
this.Description.Behaviors.Find<ServiceCredentials>();
if (serviceCredentials == null)
{
serviceCredentials = new ServiceCredentials();
this.Description.Behaviors.Add(serviceCredentials);
}
// Set the service certificate
serviceCredentials.ServiceCertificate.SetCertificate(
"CN=localhost");
/*
Setting the CertificateValidationMode to PeerOrChainTrust means that if the certificate is in the Trusted People store, then it will be trusted without performing a validation of the certificate's issuer chain. This setting is used here for convenience so that the sample can be run without having to have certificates issued by a certification authority (CA).
This setting is less secure than the default, ChainTrust. The security implications of this setting should be carefully considered before using PeerOrChainTrust in production code.
*/
serviceCredentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;
// Create the custom binding and add an endpoint to the service.
Binding multipleTokensBinding =
BindingHelper.CreateMultiFactorAuthenticationBinding();
this.AddServiceEndpoint(typeof(IEchoService),
multipleTokensBinding, string.Empty);
base.InitializeRuntime();
}
サービス コード:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class EchoService : IEchoService
{
public string Echo()
{
string userName;
string certificateSubjectName;
GetCallerIdentities(
OperationContext.Current.ServiceSecurityContext,
out userName,
out certificateSubjectName);
return $"Hello {userName}, {certificateSubjectName}";
}
public void Dispose()
{
}
bool TryGetClaimValue<TClaimResource>(ClaimSet claimSet,
string claimType, out TClaimResource resourceValue)
where TClaimResource : class
{
resourceValue = default(TClaimResource);
IEnumerable<Claim> matchingClaims =
claimSet.FindClaims(claimType, Rights.PossessProperty);
if(matchingClaims == null)
return false;
IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
if (enumerator.MoveNext())
{
resourceValue =
(enumerator.Current.Resource == null) ? null :
(enumerator.Current.Resource as TClaimResource);
return true;
}
else
{
return false;
}
}
// Returns the username and certificate subject name provided by
//the client
void GetCallerIdentities(ServiceSecurityContext
callerSecurityContext,
out string userName, out string certificateSubjectName)
{
userName = null;
certificateSubjectName = null;
// Look in all the claimsets in the authorization context
foreach (ClaimSet claimSet in
callerSecurityContext.AuthorizationContext.ClaimSets)
{
if (claimSet is WindowsClaimSet)
{
// Try to find a Name claim. This will have been
// generated from the windows username.
string tmpName;
if (TryGetClaimValue<string>(claimSet, ClaimTypes.Name,
out tmpName))
{
userName = tmpName;
}
}
else if (claimSet is X509CertificateClaimSet)
{
// Try to find an X500DistinguishedName claim. This will
// have been generated from the client certificate.
X500DistinguishedName tmpDistinguishedName;
if (TryGetClaimValue<X500DistinguishedName>(claimSet,
ClaimTypes.X500DistinguishedName,
out tmpDistinguishedName))
{
certificateSubjectName = tmpDistinguishedName.Name;
}
}
}
}
}
クライアント エンドポイントは、サービス エンドポイントと同様の方法で構成されます。 クライアントは、同じ BindingHelper
クラスを使用してバインディングを作成します。 セットアップの残りの部分は、 Client
クラスにあります。 クライアントは、ユーザー名セキュリティ トークンに関する情報、サポートする X.509 セキュリティ トークン、セットアップ コード内のサービス X.509 証明書に関する情報をクライアント エンドポイント動作コレクションに設定します。
static void Main()
{
// Create the custom binding and an endpoint address for
// the service.
Binding multipleTokensBinding =
BindingHelper.CreateMultiFactorAuthenticationBinding();
EndpointAddress serviceAddress = new EndpointAddress(
"http://localhost/servicemodelsamples/service.svc");
ChannelFactory<IEchoService> channelFactory = null;
IEchoService client = null;
Console.WriteLine("Username authentication required.");
Console.WriteLine(
"Provide a valid machine or ___domain account. [___domain\\user]");
Console.WriteLine(" Enter username:");
string username = Console.ReadLine();
Console.WriteLine(" Enter password:");
string password = "";
ConsoleKeyInfo info = Console.ReadKey(true);
while (info.Key != ConsoleKey.Enter)
{
if (info.Key != ConsoleKey.Backspace)
{
if (info.KeyChar != '\0')
{
password += info.KeyChar;
}
info = Console.ReadKey(true);
}
else if (info.Key == ConsoleKey.Backspace)
{
if (password != "")
{
password =
password.Substring(0, password.Length - 1);
}
info = Console.ReadKey(true);
}
}
for (int i = 0; i < password.Length; i++)
Console.Write("*");
Console.WriteLine();
try
{
// Create a proxy with the previously create binding and
// endpoint address
channelFactory =
new ChannelFactory<IEchoService>(
multipleTokensBinding, serviceAddress);
// configure the username credentials, the client
// certificate and the server certificate on the channel
// factory
channelFactory.Credentials.UserName.UserName = username;
channelFactory.Credentials.UserName.Password = password;
channelFactory.Credentials.ClientCertificate.SetCertificate(
"CN=client.com", StoreLocation.CurrentUser, StoreName.My);
channelFactory.Credentials.ServiceCertificate.SetDefaultCertificate(
"CN=localhost", StoreLocation.LocalMachine, StoreName.My);
client = channelFactory.CreateChannel();
Console.WriteLine("Echo service returned: {0}",
client.Echo());
((IChannel)client).Close();
channelFactory.Close();
}
catch (CommunicationException e)
{
Abort((IChannel)client, channelFactory);
// if there is a fault then print it out
FaultException fe = null;
Exception tmp = e;
while (tmp != null)
{
fe = tmp as FaultException;
if (fe != null)
{
break;
}
tmp = tmp.InnerException;
}
if (fe != null)
{
Console.WriteLine("The server sent back a fault: {0}",
fe.CreateMessageFault().Reason.GetMatchingTranslation().Text);
}
else
{
Console.WriteLine("The request failed with exception: {0}",e);
}
}
catch (TimeoutException)
{
Abort((IChannel)client, channelFactory);
Console.WriteLine("The request timed out");
}
catch (Exception e)
{
Abort((IChannel)client, channelFactory);
Console.WriteLine(
"The request failed with unexpected exception: {0}", e);
}
Console.WriteLine();
Console.WriteLine("Press <ENTER> to terminate client.");
Console.ReadLine();
}
呼び出し元の情報の表示
呼び出し元の情報を表示するには、次のコードに示すように ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
を使用します。
ServiceSecurityContext.Current.AuthorizationContext.ClaimSets
には、現在の呼び出し元に関連付けられている承認要求が含まれています。 これらの要求は、メッセージで受信したすべてのトークンに対して Windows Communication Foundation (WCF) によって自動的に提供されます。
bool TryGetClaimValue<TClaimResource>(ClaimSet claimSet, string
claimType, out TClaimResource resourceValue)
where TClaimResource : class
{
resourceValue = default(TClaimResource);
IEnumerable<Claim> matchingClaims =
claimSet.FindClaims(claimType, Rights.PossessProperty);
if (matchingClaims == null)
return false;
IEnumerator<Claim> enumerator = matchingClaims.GetEnumerator();
if (enumerator.MoveNext())
{
resourceValue = (enumerator.Current.Resource == null) ? null : (enumerator.Current.Resource as TClaimResource);
return true;
}
else
{
return false;
}
}
// Returns the username and certificate subject name provided by the client
void GetCallerIdentities(ServiceSecurityContext callerSecurityContext, out string userName, out string certificateSubjectName)
{
userName = null;
certificateSubjectName = null;
// Look in all the claimsets in the authorization context
foreach (ClaimSet claimSet in
callerSecurityContext.AuthorizationContext.ClaimSets)
{
if (claimSet is WindowsClaimSet)
{
// Try to find a Name claim. This will have been generated
//from the windows username.
string tmpName;
if (TryGetClaimValue<string>(claimSet, ClaimTypes.Name,
out tmpName))
{
userName = tmpName;
}
}
else if (claimSet is X509CertificateClaimSet)
{
//Try to find an X500DistinguishedName claim.
//This will have been generated from the client
//certificate.
X500DistinguishedName tmpDistinguishedName;
if (TryGetClaimValue<X500DistinguishedName>(claimSet,
ClaimTypes.X500DistinguishedName,
out tmpDistinguishedName))
{
certificateSubjectName = tmpDistinguishedName.Name;
}
}
}
}
サンプルの実行
サンプルを実行すると、クライアントはまず、ユーザー名トークンのユーザー名とパスワードを指定するように求められます。 サービス上の WCF は、ユーザー名トークンで指定された値をシステムによって提供される ID にマップするため、システム アカウントに正しい値を指定してください。 その後、クライアントはサービスからの応答を表示します。 クライアント ウィンドウで Enter キーを押して、クライアントをシャットダウンします。
Batch ファイルのセットアップ
このサンプルに含まれる Setup.bat バッチ ファイルを使用すると、サーバー証明書ベースのセキュリティを必要とするインターネット インフォメーション サービス (IIS) でホストされるアプリケーションを実行するように、関連する証明書を使用してサーバーを構成できます。 このバッチ ファイルは、マシン間で動作するように、またはホストされていないケースで動作するように変更する必要があります。
次に、バッチ ファイルを適切な構成で実行するように変更できるように、バッチ ファイルのさまざまなセクションの概要を示します。
クライアント証明書の作成
Setup.bat バッチ ファイルの次の行は、使用するクライアント証明書を作成します。
%CLIENT_NAME%
変数は、クライアント証明書のサブジェクトを指定します。 このサンプルでは、サブジェクト名として "client.com" を使用します。
証明書は、CurrentUser
ストアの場所の My (Personal) ストアに保存されます。
echo ************
echo making client cert
echo ************
makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%CLIENT_NAME% -sky exchange -pe
クライアント証明書をサーバーの信頼されたストアにインストールする
Setup.bat バッチ ファイルの次の行は、クライアント証明書をサーバーの信頼されたユーザー ストアにコピーします。 Makecert.exe によって生成された証明書はサーバーのシステムによって暗黙的に信頼されないため、この手順が必要です。 クライアントの信頼されたルート証明書 (Microsoft が発行した証明書など) にルート化された証明書が既にある場合、クライアント証明書ストアにサーバー証明書を設定するこの手順は必要ありません。
echo ************
echo copying client cert to server's CurrentUserstore
echo ************
certmgr.exe -add -r CurrentUser -s My -c -n %CLIENT_NAME% -r LocalMachine -s TrustedPeople
サーバー証明書の作成
Setup.bat バッチ ファイルの次の行は、使用するサーバー証明書を作成します。
%SERVER_NAME%
変数は、サーバー名を指定します。 この変数を変更して、独自のサーバー名を指定します。 このバッチ ファイルの既定値は localhost です。
証明書は、ローカルマシン ストアの場所にあるマイ (個人用) ストアに格納されます。 証明書は、IIS でホストされるサービスの LocalMachine ストアに格納されます。 セルフホステッド サービスの場合は、LocalMachine という文字列を CurrentUser に置き換えて、CurrentUser ストアの場所にサーバー証明書を格納するようにバッチ ファイルを変更する必要があります。
echo ************
echo Server cert setup starting
echo %SERVER_NAME%
echo ************
echo making server cert
echo ************
makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe
クライアントの信頼された証明書ストアへのサーバー証明書のインストール
Setup.bat バッチ ファイル内の次の行は、クライアントの信頼できるユーザー ストアにサーバー証明書をコピーします。 Makecert.exe によって生成された証明書はクライアント システムによって暗黙的に信頼されないため、この手順が必要です。 クライアントの信頼されたルート証明書 (Microsoft が発行した証明書など) にルート化された証明書が既にある場合、クライアント証明書ストアにサーバー証明書を設定するこの手順は必要ありません。
echo ************
echo copying server cert to client's TrustedPeople store
echo ************certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople
証明書の秘密キーへのアクセスの有効化
IIS でホストされるサービスから証明書の秘密キーへのアクセスを有効にするには、IIS でホストされるプロセスが実行されているユーザー アカウントに秘密キーに対する適切なアクセス許可が付与されている必要があります。 これは、Setup.bat スクリプトの最後の手順で行います。
echo ************
echo setting privileges on server certificates
echo ************
for /F "delims=" %%i in ('"%ProgramFiles%\ServiceModelSampleTools\FindPrivateKey.exe" My LocalMachine -n CN^=%SERVER_NAME% -a') do set PRIVATE_KEY_FILE=%%i
set WP_ACCOUNT=NT AUTHORITY\NETWORK SERVICE
(ver | findstr /C:"5.1") && set WP_ACCOUNT=%COMPUTERNAME%\ASPNET
echo Y|cacls.exe "%PRIVATE_KEY_FILE%" /E /G "%WP_ACCOUNT%":R
iisreset
サンプルを設定、ビルド、実行するには
Windows Communication Foundation サンプルのOne-Time セットアップ手順を実行していることを確認します。
ソリューションをビルドするには、「 Windows Communication Foundation サンプルのビルド」の手順に従います。
単一または複数のコンピューター間の構成でサンプルを実行するには、次の手順に従います。
同じコンピューターでサンプルを実行するには
Visual Studio コマンド プロンプト内のサンプル インストール フォルダーから Setup.bat を実行し、管理者特権で実行します。 これにより、サンプルの実行に必要なすべての証明書がインストールされます。
注
Setup.bat バッチ ファイルは、Visual Studio コマンド プロンプトから実行するように設計されています。 Visual Studio コマンド プロンプト内で設定された PATH 環境変数は、Setup.bat スクリプトに必要な実行可能ファイルを含むディレクトリを指します。 サンプルが完了したら、Cleanup.bat を実行して証明書を削除してください。 他のセキュリティ サンプルでは、同じ証明書が使用されます。
\client\bin から Client.exe を起動します。 クライアント アクティビティがクライアント コンソール アプリケーションに表示されます。
クライアントとサービスが通信できない場合は、「WCF サンプルのトラブルシューティングのヒント」を参照してください。
マシン間でサンプルを実行するには
サービス マシンにディレクトリを作成します。 インターネット インフォメーション サービス (IIS) 管理ツールを使用して、このディレクトリの servicemodelsamples という名前の仮想アプリケーションを作成します。
サービス プログラム ファイルを \inetpub\wwwroot\servicemodelsamples からサービス マシン上の仮想ディレクトリにコピーします。 \bin サブディレクトリ内のファイルをコピーしてください。 また、Setup.bat、Cleanup.bat、および ImportClientCert.bat ファイルをサービス コンピューターにコピーします。
クライアント バイナリ用のディレクトリをクライアント コンピューターに作成します。
クライアント コンピューター上のクライアント ディレクトリにクライアント プログラム ファイルをコピーします。 また、Setup.bat、Cleanup.bat、および ImportServiceCert.bat ファイルをクライアントにコピーします。
サーバーで、管理者特権で開かれた Visual Studio の開発者コマンド プロンプトで
setup.bat service
を実行します。setup.bat
引数を指定してservice
を実行すると、マシンの完全修飾ドメイン名を持つサービス証明書が作成され、サービス証明書が Service.cer という名前のファイルにエクスポートされます。Web.config を編集して、新しい証明書名を (> の 属性に) 反映させます。これは、マシンの完全修飾ドメイン名と同じです。
Service.cer ファイルをサービス ディレクトリからクライアント コンピューター上のクライアント ディレクトリにコピーします。
クライアントで、管理者特権で開かれた Visual Studio の開発者コマンド プロンプトで
setup.bat client
を実行します。setup.bat
引数でclient
を実行すると、client.com という名前のクライアント証明書が作成され、クライアント証明書が Client.cer という名前のファイルにエクスポートされます。クライアント コンピューター上の Client.exe.config ファイルで、サービスの新しいアドレスと一致するようにエンドポイントのアドレス値を変更します。 これを行うには、localhost をサーバーの完全修飾ドメイン名に置き換えます。
Client.cer ファイルをクライアント ディレクトリからサーバー上のサービス ディレクトリにコピーします。
クライアントで、ImportServiceCert.bat実行します。 これにより、Service.cer ファイルから CurrentUser - TrustedPeople ストアにサービス証明書がインポートされます。
サーバーで、ImportClientCert.bat実行します。これにより、Client.cer ファイルから LocalMachine - TrustedPeople ストアにクライアント証明書がインポートされます。
クライアント コンピューターで、コマンド プロンプト ウィンドウから Client.exe を起動します。 クライアントとサービスが通信できない場合は、「WCF サンプルのトラブルシューティングのヒント」を参照してください。
サンプルの実行後にクリーンアップするには
- サンプルの実行が完了したら、samples フォルダーで Cleanup.bat を実行します。
注
このスクリプトでは、複数のコンピューターでこのサンプルを実行する場合、クライアント上のサービス証明書は削除されません。 コンピューター間で証明書を使用する WCF サンプルを実行している場合は、CurrentUser - TrustedPeople ストアにインストールされているサービス証明書を必ずクリアしてください。 これを行うには、次のコマンドを使用します。 certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name>
例: certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com
.