前回の続きです。前回はこちら → Azure AppFabric ACS v1+ADFS+WCF Data Services
今回は実際にフェデレーション認証を利用するサービスとクライアントを作成します。
サービス側はWCF Data Servicesを利用して、トークンが有効な場合のみデータ(Entity)を返すようにします。
クライアントはActive Federation認証を行いますので、ADFS2.0とACSv1を使用し得られたトークンと利用してWCF Data Servicesにアクセスしデータを取得します。
ではまずサービス側から。
WCF Data Servicesの準備
ではサクッとプロジェクトを作成します。
「ASP.NET 空のWebアプリケーション」を指定します。
作成後Entity Data Modelを追加します。
ウィザードが起動するので、サンプルで作っておいた(作っておいてください)のDBを指定していきます。
さて次にACSv1から発行されたトークン(受け取ったトークン)を検証するためのクラスを追加します。
基本的にはハンズオンラボ内にあるACSTOkenManagerそのままで問題ありませんが、受け取ったクレームを列挙し確認する部分が今回のサンプルでは邪魔なのでコメントアウトでもしときます。
※受け取った値が正しいかどうか等、自前の検証ロジックを追加してもいいですね。
※ハンズオンラボは \IdentityTrainingKitVS2010\Labs\IntroAppFabricAccessControl\Source\Ex02-UsingACWithSAMLTokens\End フォルダにあるのを参考に。
またSetUnauthorizedResponse()メソッドで検証失敗時にアクセス拒否なレスポンスを返していますがとりあえず削除して無視します。(今回はクエリーインターセプターでFalseで返すようにするので)
まぁこの辺は改善の余地ありですね。
次に「WCF Data Serivces」を追加します。
公開するEntityをToDoに従い修正して、クエリーインターセプターを追加します。
クエリーインターセプター内ではHTTPリクエストヘッダ内にあるAuthorizationを取得します。(このヘッダにトークンが含まれます)
またトークンを検証するロジックを追加します。
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;
using System.Linq.Expressions;
namespace WCFDataServicesSample
{
public class Sample : DataService<SampleDBEntities>
{
// このメソッドは、サービス全体のポリシーを初期化するために、1 度だけ呼び出されます。
public static void InitializeService(DataServiceConfiguration config)
{
// TODO: 表示や更新などが可能なエンティティ セットおよびサービス操作を示す規則を設定してください
// 例:
config.SetEntitySetAccessRule("TwitterUsers", EntitySetRights.AllRead);
// config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
[QueryInterceptor("TwitterUsers")]
public Expression<Func<TwitterUsers, bool>> TwitterUsersFilter()
{
string IssuerName = "https://buchizo.accesscontrol.windows.net/";
string TokenPolicyKey = "EgZ8eqTtBpryvrIyIp41+XKdicUfFAjxsHZthebBu7c=";
string RequiredClaimType = "name";
string Audience = "http://localhost:8000";
string acsToken = HttpContext.Current.Request.Headers["Authorization"];
ACSTOkenManager tv = new ACSTOkenManager(IssuerName, Audience, Convert.FromBase64String(TokenPolicyKey), RequiredClaimType);
if (tv.CheckAccessCore(acsToken))
{
return (TwitterUsers a) => true;
}
else
{
return (TwitterUsers a) => false;
}
}
}
}
最後にWCF Data Servicesが指定したURLで動作するように、プロジェクトの設定からポート番号の設定などを行います。
Issuer等でURLを見るので、ACSv1のScopeで設定した内容と一致しないと正しくトークンが発行されない/検証に失敗するので注意。
WCF Data Services側はこんな感じで終わりです。
トークンを受け取った後、分解して有効期限内か、Issuerが意図したものか、ハッシュは正しいか等を確認します。(不正なトークンで改ざんされていないかを確認してます)
では次はクライアント側ですね。
クライアントアプリケーションの準備
クライアントはコンソールアプリで試すことにします。
まず以下のアセンブリを追加します。
- Microsoft.IdentityModel
- System.IdentityModel
- System.ServiceModel.Web
- System.Web
※Microsoft.IdentityModel はWIF SDK 4.0に含まれています。
次にWCF Data ServicesのProxyを生成します。
サービス参照の追加を選択し、URLを入力して追加します。
一応、接続先のURLやらADFS/ACSv1のサービス名やらは設定ファイルに出すことにします。
次にADFS/ACSv1へアクセスしトークンをもらうクラスを適当に作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.IdentityModel.Protocols.WSTrust;
using Microsoft.IdentityModel.Protocols.WSTrust.Bindings;
using System.IdentityModel.Tokens;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.ServiceModel.Web;
using System.Net;
using System.Collections.Specialized;
namespace SampleClient
{
public class TokenAccessor
{
public static string GetSamlAssertion(string stsAddress, string acsStsAddress)
{
WSTrustChannelFactory trustChannelFactory = new WSTrustChannelFactory(
new WindowsWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(new Uri(stsAddress)));
trustChannelFactory.TrustVersion = TrustVersion.WSTrust13;
RequestSecurityToken rst = new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue, WSTrust13Constants.KeyTypes.Bearer);
rst.AppliesTo = new EndpointAddress(acsStsAddress);
rst.TokenType = Microsoft.IdentityModel.Tokens.SecurityTokenTypes.Saml2TokenProfile11;
WSTrustChannel channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
GenericXmlSecurityToken token = channel.Issue(rst) as GenericXmlSecurityToken;
return token.TokenXml.OuterXml;
}
public static string GetACSToken(string samlAssertion)
{
WebClient tokenClient = new WebClient();
tokenClient.BaseAddress = string.Format("https://{0}.{1}", Properties.Settings.Default.ServiceNamespace, Properties.Settings.Default.AcsHostName);
NameValueCollection values = new NameValueCollection();
values.Add("wrap_assertion_format", "SAML");
values.Add("wrap_assertion", samlAssertion);
values.Add("wrap_scope", Properties.Settings.Default.AudienceUrl);
byte[] responseBytes = tokenClient.UploadValues("WRAPv0.9", "POST", values);
string response = Encoding.UTF8.GetString(responseBytes);
return response
.Split('&')
.Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase))
.Split('=')[1];
}
}
}
このコードもハンズオンラボから持って来れば動きます。(パラメータ部分を設定ファイルから取得したり、少しは修正していますが)
次に、最終的に取得したトークンを含めてWCF Data Servicesにアクセスできるように、EntitiesのPartialクラスを作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Services.Client;
using System.Web;
namespace SampleClient.ServiceReference1
{
public partial class SampleDBEntities
{
public string ACSToken { get; set; }
partial void OnContextCreated()
{
this.SendingRequest += new EventHandler<SendingRequestEventArgs>(OnSendingRequest);
}
public void OnSendingRequest(object sender, SendingRequestEventArgs e)
{
if (ACSToken != "")
{
string authHeaderValue = string.Format("WRAP access_token=\"{0}\"", HttpUtility.UrlDecode(ACSToken));
e.RequestHeaders.Add("authorization", authHeaderValue);
}
}
}
}
SendingRequestイベントを追加して実際にリクエストを送信する際にHTTPヘッダにAuthorizationを追加し、トークンを渡すようにしています。
後はメインの部分。
- ADFSからトークンをもらう(ADFSへの認証は今回はWindows統合認証とします)
- ADFSから受け取ったSAMLトークンをACSv1に渡してトークンもらう
- 受け取ったトークンをEntityに渡してWCF Data Servicesからデータを取得する
といったコードを書きます。
一応、確認のためにトークンあり/なしの2パターンで実行します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
namespace SampleClient
{
class Program
{
static void Main(string[] args)
{
try
{
Console.ReadKey(); //WCF Data Services起動待機用
string stsAddress = string.Format("https://{0}/{1}", Properties.Settings.Default.StsBaseAddress, Properties.Settings.Default.StsPath);
string acsSTSAddress = string.Format("https://{0}.{1}/WRAPv0.9", Properties.Settings.Default.ServiceNamespace, Properties.Settings.Default.AcsHostName);
//ADFSからトークン取得
string samlAssertion = TokenAccessor.GetSamlAssertion(stsAddress, acsSTSAddress);
Console.WriteLine("= GetSamlAssertion =======================\r\n" + samlAssertion);
//ACSv1からトークン取得
string acsToken;
acsToken = TokenAccessor.GetACSToken(samlAssertion);
Console.WriteLine("= GetACSToken ============================\r\n" + acsToken);
//WCF Data Servicesからデータ取得
ServiceReference1.SampleDBEntities ent = new ServiceReference1.SampleDBEntities(new Uri(Properties.Settings.Default.WebServiceUrl));
//結果を出力(トークンなし)
Console.WriteLine("= Get Entity =");
foreach (var user in ent.TwitterUsers)
{
Console.WriteLine("ID:{0}\tName:{1}", user.TwitterID, user.DisplayName);
}
Console.WriteLine("\r\nTotal:{0}\r\n",ent.TwitterUsers.Count());
//結果を出力(トークンあり)
Console.WriteLine("= Get Entity with ACS token =");
ent.ACSToken = acsToken;
foreach (var user in ent.TwitterUsers)
{
Console.WriteLine("ID:{0}\tName:{1}", user.TwitterID, user.DisplayName);
}
Console.WriteLine("\r\nTotal:{0}\r\n", ent.TwitterUsers.Count());
Console.WriteLine("= done =");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
}
ACMBrowserで設定したトークンの有効期限内であれば、トークンは再利用できます。
実際はトークンの期限切れかどうかの確認や、アクセス拒否された際の再試行処理等いろいろ考えないといけないかとは思いますがまぁサンプルなので。
以上でクライアント側も終了です。
動作確認
では実際に動作させてみましょう。デバッグでもいいですが、ちゃんとWCF Data Serviecsが起動していることを確認ください。
正しく設定されていればちゃんとトークンを取得して最終的にWCF Data Servicesからデータを取得できることがわかります。
またトークンが無い場合は空データですね。(そのようにクエリーインターセプターで設定したので)
まとめ
ハンズオンラボの内容そのままな感じですが、ACSv1とADFSをどう連携させるかが何となくわかったかなと思います。今回、トークンを最終的にサービスに渡していますが、トークンの有効期限内は再度取得する必要はありません。(トークンの有効期限切れの処理はもう少しコードを追加しないといけないでしょう)
今回はWCF Data Servicesで試しましたが、ASP.NET MVCなどのWebアプリケーションでも同じようにできます。ただ、ACSv1がPassive FederationできないのでWebアプリがProxy的に動作するイメージになるかと。ログオン画面でID/Passをもらった後、カスタムのMembershipProviderでoverrideしたValidateUserメソッド内でADFS、ACSv1へトークンの受け渡しを行い最終的にトークンの検証をすれば動くと思います。
※Windows認証使わない場合(ID/Passで認証する場合)は、ADFSのエンドポイントを”Trust/13/usernamemixed”等にし、WSTrustChannelFactoryのCredentials.UserNameに(ADの)IDとパスワードを指定すればいいですね)
ま、そうそう使う人は居なさそうですけどこんなこともできるんだな程度で。
※CardSpaceがありゃぁなぁとか思ってると次はU-Proveなんですよね。(参考:さようなら CardSpace、こんにちは U-Prove!)














