Windows Azure Authentication Libraryを使ってみた

あまり話題に挙がってない気がするWindows Azure Authentication Libraryをさわってみました。

※必要となる前提知識が多くて用語とか微妙な部分とか、おいら自身もまだ怪しい部分があるので要素要素の詳細は各自で調べて頂けると嬉しいです。あとツッコミあれば。

で、触る前にWindows Azure Authentication Library(以下AAL)ってなんでしょう?

MSDNブログの説明を見ると「AAL は .NET ベースのクライアントアプリケーションやサービスで Windows Azure AD や他の認証プロバイダーを利用したユーザー/アプリケーション認証を利用するためのライブラリで、NuGet PowerShell 経由で直接 Visual Studio に追加できます。」とあります。

とりあえず.NET用のライブラリってのはわかりました。んで肝心の部分ですが、簡単に言うと今までACS使ってフェデレーション認証したりするのに面倒くさい(特にリッチクライアント用とか)コードをたくさん書かないといけなかったのを、すごく簡略化してくれる大変ありがたいライブラリなのです!

※Webアプリは平たく言うとリダイレクトの処理するだけだし。。

具体的にリッチクライアントでフェデレーション認証するのに書かないといけないコードはたったの数行です!スバラシイ!

AALがないときー

image

 

AALがあるときー

image

 

という感じなんですよ!

まぁWindows Azure Active DirectoryとかGraph APIとか、OAuth2.0がどうとかWS-Federationがどうとか細かいのは各自調べて頂くとして、さっそくAALを使ってシンプルなフェデレーション認証を使ったWebAPIとクライアントを実装してどれぐらい楽なのか見てみましょう。

※今回想定するアプリの構成は以下のような感じです。

image

だいぶ端折ってますが。

 

下準備

まずはACSv2を使ってIdPやRelyingParty(証明書利用者アプリケーション)を定義しておきましょう。

今回、IdPには(別になんでもいいんですが)Windows Live ID、Google、Yahoo!を追加しておきます。次に証明書利用者アプリケーションですが、以下の例のように作成します。

領域(Realm)は最終的にアクセスするWebサービスとかのURLです。あと重要なのはトークン形式ですが、AALは今のところJWT(JsonWebToken)でないとダメなようです。

あと初期値だと規則グループないので作るようにしましょう。(トークン署名キーはX.509と対称キーの両方がプライマリ状態だとうまく動作しなかった気がする)

次に規則グループですが、特に何も考えずに今回は自動生成に任せてしまいます。

ACS側はだいたいこんな感じで終わりです。

Webサービス側の実装

Webサービス側はサンプルをベースに持ってこようと思います。具体的にはASP.NET MVC4でWebAPIなアプリを新規作成します。

で、NuGetを使ってMicrosoft.WindowsAzure.ActiveDirectory.Authentication を追加します。

x86 か x64 かは環境に合わせて追加しましょう。

次に参照の追加でMicrosoft.IdentityModelも追加しておきます。あ、ローカルコピーはTrueとかにしておきましょうね。

さてサンプルに倣ってGlobal.asax.csに以下のようにしときましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Microsoft.WindowsAzure.ActiveDirectory.Authentication;

namespace AALSampleWebAPI
{
	// Note: For instructions on enabling IIS6 or IIS7 classic mode, 
	// visit http://go.microsoft.com/?LinkId=9394801

	public class WebApiApplication : System.Web.HttpApplication
	{
		static void Configure(HttpConfiguration config)
		{
			config.MessageHandlers.Add(new TokenValidationHandler());
		}
		
		protected void Application_Start()
		{
			AreaRegistration.RegisterAllAreas();

			FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
			RouteConfig.RegisterRoutes(RouteTable.Routes);
			BundleConfig.RegisterBundles(BundleTable.Bundles);
			Configure(GlobalConfiguration.Configuration);
		}
	}
	internal class TokenValidationHandler : DelegatingHandler
	{
		// This function retrieves ACS token (in format of OAuth 2.0 Bearer Token type) from 
		// the Authorization header in the incoming HTTP request from the ShipperClient.
		private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
		{
			token = null;
			IEnumerable<string> authzHeaders;
			if (!request.Headers.TryGetValues("Authorization", out authzHeaders) || authzHeaders.Count() > 1)
			{
				// Fail if no Authorization header or more than one Authorization headers 
				// are found in the HTTP request 
				return false;
			}

			// Remove the bearer token scheme prefix and return the rest as ACS token 
			var bearerToken = authzHeaders.ElementAt(0);
			token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
			return true;
		}

		protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
		{
			HttpStatusCode statusCode;
			string token;

			if (!TryRetrieveToken(request, out token))
			{
				statusCode = HttpStatusCode.Unauthorized;
				return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
			}

			try
			{
				// Create an AAL AuthenticationContext object and link it to the tenant backing the ShipperService
				var authenticationContext = new AuthenticationContext("https://buchizoaal.accesscontrol.windows.net");

				// Configure the expected audience list for AuthenticationContext to validate the token. AuthenticationContext
				// will auto-retrieve the expected signing cert and issuer from the tenent's metadata endpoint to assist token
				// validation as well.
				authenticationContext.Options.Audiences.Add("http://localhost:5120/");

				// Invoke AuthenticationContext.AcceptToken to validate the token and receive a ClaimPrincipal on a successful validation
				Thread.CurrentPrincipal = authenticationContext.AcceptToken(token);

				return base.SendAsync(request, cancellationToken);
			}
			catch (AALAuthenticationException)
			{
				statusCode = HttpStatusCode.Unauthorized;
			}
			catch (WebException we)
			{
				statusCode = ((HttpWebResponse)we.Response).StatusCode;
			}
			catch (ArgumentException)
			{
				statusCode = HttpStatusCode.BadRequest;
			}
			catch (AALException)
			{
				statusCode = HttpStatusCode.Unauthorized;
			}
			catch (Exception)
			{
				statusCode = HttpStatusCode.InternalServerError;
			}
			return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
		}
	}
}

 

HttpConfigurationのメッセージハンドラにTokenValidationHandlerを追加してるだけです。

TokenValidationHandlerでは認証ヘッダからトークンを取得し問題がなければ現在のスレッドのプリンシパルにトークンから得られた情報を設定します。(MVCのコントローラとかから見ると認証済みになってUserプロパティにユーザー名などが入った状態になります)

さて注目したいのはAuthenticationContextを生成してるところからの3つのコードですね。AuthenticationContextのコンストラクタに渡してるのはACSのURLです。ここで認可されたトークンを処理するためのコンテキストを生成すると思っておいたらいいかもです。

次にサービス先(最終的にトークンを渡す相手)となるURL(つまりこのWebサービスのURL)を設定します。

あとは受け取ったトークンからAcceptTokenメソッドを使って、プリンシパルを取得するだけです。ACSから指定したサービス先のために発行されてるかの検証など面倒くさいのは全部やってくれます。素晴らしい!

まぁこのサンプルの方法だと全部トークンがないとダメになるので認証プロバイダを作るほうがいいのかもですね。やることは対して変わらないですし。まぁその辺はサービスの設計時にうまく考えましょう。

あ、トークンはAuthorizationヘッダにOAuth2.0のBearerトークンとしてくるのでBearer部分を除去しておきましょう。

クライアント側の実装

次はクライアント側の実装です。今回はWPFでサクッと作ってみます。

Webサービス側と同じようにAALとMicrosoft.IdentityModelを追加しておきましょう。

細かいことは置いといてボタンクリックされたら認証してWebAPIから値をとってくるという単純なものです。

		private AssertionCredential assertionCredential;
		private AuthenticationContext authContext;
		private string ServiceHost = "http://localhost:5120/";

		private void AuthButton_Click(object sender, RoutedEventArgs e)
		{
			try
			{
				authContext = new AuthenticationContext("https://buchizoaal.accesscontrol.windows.net");
				authContext.Options.Audiences.Add(ServiceHost);

				//認証ダイアログ(ブラウザダイアログ)を表示する
				using (assertionCredential = authContext.AcquireUserCredentialUsingUI(ServiceHost))
				{
					if (assertionCredential == null) return;

					Output.Text += "\nAuthentication Success =============";
					Output.Text += "\nAssertion: " + assertionCredential.Assertion;
					Output.Text += "\nAssertionType: " + assertionCredential.AssertionType;

					//クレーム情報を表示するだけ(本題とあまり関係ない)
					var claimsPrincipal = authContext.AcceptToken(assertionCredential.Assertion);
					var Identity = claimsPrincipal.Identity as Microsoft.IdentityModel.Claims.ClaimsIdentity;
					Identity.Claims.ToList().ForEach(x => { Output.Text += "\n" + x.ClaimType + ": " + x.Value.ToString(); });

					var request = WebRequest.Create(new Uri(ServiceHost + "api/Values")) as HttpWebRequest;
					request.Method = "GET";
					request.ContentLength = 0;

					//認証ヘッダを生成する(OAuth2.0 Bearerトークンを生成して設定してる)
					request.Headers.Add("Authorization", assertionCredential.CreateAuthorizationHeader());

					using (var response = request.GetResponse() as HttpWebResponse)
					{
						using (var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
						{
							var responseContent = sr.ReadToEnd();
							Output.Text += "\nContent: " + responseContent;
						}
					}
				}
			}
			catch(AALAuthenticationException aex)
			{
				Output.Text += "\n---------------\nAALAuthenticationException: " + aex.Message;
				Output.Text += "\nServiceErrorMessage: " + aex.ServiceErrorMessage;
				if (aex.InnerException != null)
				{
					Output.Text += "\nInnerException: " + aex.InnerException.Message;
				}
			}
			catch (Exception ex)
			{
				Output.Text += "\n---------------\nException: " + ex.Message;
			}
		}

 

実質AALに関するところは4行ってところでしょうか。やってることはWebサービス側とあまり変わりません。クライアント側の大きな違いですが、AcquireUserCredentialUsingUIを呼び出すと認証用のブラウザダイアログが表示されて認証されるかキャンセルすると制御が戻ってくるという素晴らしく便利なメソッドが用意されてます。あとはCreateAuthorizationHeaderメソッドを使ってトークンを生成してるぐらいですね。

さてさて、これで準備できたわけです。実際に試してみましょう!

動作確認

では実行してみましょう。

WPFなクライアントアプリでボタンクリックしたら認証ダイアログが表示されます。(ACSやもろもろの設定が正しければ)

 

おなじみですね!でもこれコーディングしてないんですよ!。で、指定した認証プロバイダで認証すると、問題なければトークンを取得してWebサービスにアクセスしてJsonな値を受け取ります。

 

Assertionとして表示されている値が最終的にACSが返してくれたトークンです。クレーム情報はACSで設定したとおり返ってきてるはずです。(Googleにしたのは表示名やメアドが取得できるから)

仕組みは面倒ですが、このあたりの仕組みが隠ぺいされるのはコードする側にとっては楽でいいですね!(設定する側はあれですけど)

さてこれだけだとあれなので、もう少しプロセスを見てみたいと思います。

コードでは大したことしてませんが、図にするとこんな感じです。

image

※面倒だったのでWindows Azure WebサイトにWebサービスを発行してます

 

(1)(2) … AcquireUserCredentialUsingUIを呼び出すと、AuthenticationContextで指定したテナントサーバー(ACS)にIdPの一覧を要求します。ACSはJavaScriptで(JSONで)一覧を返します。

 

(3)(4) … ブラウザダイアログで選択された認証プロバイダ(IdP)にアクセスして認証を行い、IdPの認証トークンを返してもらう。

(5)(6) …受け取ったトークンをACSに返して認可してもらう。(サービスにアクセスするためのトークンを返してもらう)

(7) … ACSから返されたトークンをAuthorizationヘッダに変えてサービスにアクセスする。

 

大事なのはAcquireUserCredentialUsingUIを呼び出した以降の(1)~(6)の通信に関するコーディングをしていないということですね。今まで全部自分でハンドリングしないといけなかったことを考えると物凄く楽ちんです!!

おまけ

クライアントじゃなくてサービスとか予め認証済みにしておきたいプログラムとかも事前にキーを配布しておくことでOKにする方法があります。(Service to Serviceとかのシナリオの内容)

image

こんな感じのパターンですか。

Windows Azure Active Directoryのようにサービスプリンシパルや、ACSにサービスIDを作っておけばこのようにアクセスを許可することができます。(キーを知っていれば許可されたもの的な感じですかね)

ACSでやる場合は事前にサービスIDを作っておきましょう。

 

この画面で表示されているサービスIDとキーを控えておきます。
クライアントはAcquireUserCredentialUsingUIで資格情報を得る代わりに、SymmetricKeyCredentialを使ってAcquireTokenメソッドから資格情報を取得します。たとえばコンソールアプリだと以下のようになります。

using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Microsoft.WindowsAzure.ActiveDirectory.Authentication;

namespace AALConsoleClient
{
	class Program
	{

		static void Main(string[] args)
		{
			AssertionCredential assertionCredential;
			AuthenticationContext authContext;
			string ServiceHost = "http://localhost:5120/";
			try
			{
				authContext = new AuthenticationContext("https://buchizoaal.accesscontrol.windows.net");
				authContext.Options.Audiences.Add(ServiceHost);
				SymmetricKeyCredential credential = new SymmetricKeyCredential("<ACSのサービスID>", Convert.FromBase64String("<ACSのサービスIDのキー>"));
				using (assertionCredential = authContext.AcquireToken(ServiceHost, credential))
				{
					if (assertionCredential == null) return;

					Console.WriteLine("Authentication Success =============");
					Console.WriteLine("Assertion: " + assertionCredential.Assertion);
					var claimsPrincipal = authContext.AcceptToken(assertionCredential.Assertion);
					var Identity = claimsPrincipal.Identity as Microsoft.IdentityModel.Claims.ClaimsIdentity;
					Identity.Claims.ToList().ForEach(x => { Console.WriteLine(x.ClaimType + ": " + x.Value.ToString()); });

					var request = WebRequest.Create(new Uri(ServiceHost + "api/Values")) as HttpWebRequest;
					request.Method = "GET";
					request.ContentLength = 0;
					request.Headers.Add("Authorization", assertionCredential.CreateAuthorizationHeader());

					using (var response = request.GetResponse() as HttpWebResponse)
					{
						using (var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
						{
							var responseContent = sr.ReadToEnd();
							Console.WriteLine("Content: " + responseContent);
						}
					}
				}
			}
			catch(AALAuthenticationException aex)
			{
				Console.WriteLine(aex.Message);
				Console.WriteLine(aex.ServiceErrorMessage);
				if (aex.InnerException != null)
				{
					Console.WriteLine(aex.InnerException.Message);
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
			Console.ReadKey();
		}
	}
}

WPFで作った場合とあまり変わりませんが、資格情報を生成する部分が異なっています。あとは一緒ですね。

というわけでサービス向けにIDを発行したり併用したりもできます。

あとサポートされている内容としては現在の資格情報(Windowsにログオンしているユーザーの資格情報)を使用してADFSにKerberos認証するとか、Windows Azure Active Directoryを使うとかありますので是非いろいろ試してみましょう。

 

まとめ

AALを使うと何となく認証部分や認可部分がスマートになるぞ!

 

参考

 

Windows Azure Authentication Libraryを使ってみた」への1件のフィードバック

  1. ピンバック: Azure ADに登録されているAPI用のアクセストークンをJWTで取得するには | hrendoh's memo

コメントを残す