セールス: 1-800-867-1380

Azure AD を使用して Windows ストア アプリケーションと REST Web サービスをセキュリティで保護する (プレビュー)

発行: 2013年4月

更新日: 2014年5月

noteメモ
このサンプルは有効期限が切れています。テクノロジ、メソッド、またはインターフェイスの命令、あるいはこれらがすべて新しい機能に置き換えられています。類似したアプリケーションを構築する更新済みのサンプルについては、「NativeClient-WindowsStore」を参照してください。

このチュートリアルでは、Azure AD ユーザーの代理で Web API を呼び出す権限を持つ Windows ストア アプリケーションを開発する方法について説明します。ネイティブ クライアント アプリケーションと Web API のいずれも、テナント管理者によって Azure AD に登録されています。この管理者は、クライアントとサービスの間の関係を、それぞれに対する個別の権限の割り当ても含めて、制御できます。認証は OAuth 2.0 ベアラーの許可の種類を使用して処理されます。また、Azure AD が更新トークンを発行するため、ユーザーが資格情報を再入力する頻度を最小限にすることができます。

このチュートリアルの完成品は、MSDN コード ギャラリーに掲載されている Windows ストア用 AAL のコード サンプルとよく似ています。コード サンプル: Windows ストア アプリケーションから REST サービス - ブラウザー ダイアログを介した AAD による認証

このチュートリアルの内容は次のとおりです。

  • リソース Web API の構築。このサービスは、Azure AD 認証を必要とする MVC 4 Web API になります。

  • クライアント アプリケーションの作成。ネイティブ クライアントは Windows ストア アプリケーションになります。

  • Azure AD によるネイティブ クライアントと Web API の登録。ネイティブ クライアントには、Web API を呼び出すアクセス権も付与されます。

noteメモ
このチュートリアルでは、開発者向けプレビュー段階の Windows ストア用 AAL を使用します。プレリリース製品であるため、本製品の機能は今後のリリースで変更されることがあります。

チュートリアルを開始する前に、次の前提条件を満たす必要があります。

  • インターネット接続

  • アクティブな Azure サブスクリプション:こちらから 90 日間無料評価版を取得できます。Azure 無料評価版

  • Visual Studio 2012 Professional または Visual Studio 2012 Ultimate:こちらから無料評価版をダウンロードできます。Visual Studio 無料評価版

  • Windows 8:このアプリケーションを開発するには、Windows 8 を使用し、開発者ライセンスが登録済みである必要があります。詳細情報

Azure AD とネイティブ クライアントの使用例を説明するために、ここでは、Azure Authentication Library (AAL) を使用して、ユーザーが Azure AD アカウントで REST サービスへのアクセス権を付与できる Windows ストア アプリケーションを構築します。

また、チュートリアルの中で、クライアントから呼び出すことができるシンプルなサービスも構築します。ユーザーはこのサービスを使用して、中央の場所に ToDo リストを保存できます。このサービスは、オンプレミスまたは Azure でホストできます。

次の図では関係するパーティの役割を示します。

  1. Contoso ToDo List Service。ユーザーの ToDo リスト項目を保存します。ユーザーが自分の項目を保存し、取得することができる Web API です。ユーザーは別のユーザーのアイテムを管理できません。

  2. Contoso ToDo List Client アプリケーション。ユーザーは、Azure AD を使用してサービスにアクセスし、ToDo リスト項目を表示および作成できます。

  3. 組織が作成する Azure AD テナント (この例では Contoso)。テナントには、ユーザー、アプリケーション、およびサービスのセキュリティ プリンシパルが含まれます。

ソリューション アーキテクチャ

このチュートリアルの最初の手順は、Azure の管理ポータルで Azure AD テナントを操作できることを確認することです。

Azure Active Directory は、Office 365 や Intune などの Microsoft 製品の ID バックボーンを提供するサービスです。このようなサービスをサブスクライブしている場合は、ユーザーがサインインに使用できる Azure AD テナントを既に持っています。そのテナントを再利用するには、現在のところ、新しい Azure サブスクリプションを作成し、サインアップ時にそのテナントの AD 管理者を使用する方法があります。このチュートリアルでは、そのプロセスの実行方法について詳しく説明しません。

このチュートリアルでは、既存の Azure AD テナントがない場合を中心に説明します。最初の手順は、Azure の管理ポータルで Azure AD テナントを操作できることを確認することです。Azure の管理ポータルには、管理ポータルのページから新しいテナントを直接作成するメカニズムがあります。

  1. http://manage.windowsazure.com に移動し、Azure サブスクリプションに関連付けられている Microsoft アカウントでサインインします。サインインしたら、画面の左側にあるタブ一覧から、[Active Directory] タブをクリックします。



    エンタープライズ ディレクトリの追加

    この画面は、Azure Management Portal の Active Directory のホーム ページです。上部の領域では 2 つの異なるヘッダーが提供されます。[アクセス制御名前空間] ヘッダーは、ACS 2.0 サービスで作成される名前空間を示します。このチュートリアルではこの部分には注目しません。[ディレクトリ] ヘッダーは、管理ポータルのディレクトリ テナント操作機能に関する部分に移動します。ほとんどのアクティビティはここで実行します。

  2. 上記のスクリーンショットに示すように、Azure Management Portal は、ユーザーに関連付けられているディレクトリ テナントがないことを検知し、作成する機会を提供します。[ディレクトリの作成] をクリックします。



    エンタープライズ テナントの追加

    ディレクトリの作成に必要な基本情報を自動的に収集するダイアログが表示されます。下から上に以下のフィールドがあります。

    • 組織名: このフィールドは必須であり、その値は会社名を表示する必要がある場合に常にモニカーとして使用されます。

    • 国または地域: このドロップダウンで選択した値により、テナントが作成される場所が決まります。ディレクトリには機密情報が格納されるので、会社がビジネスを行う国のプライバシーに関する基準を考慮してください。

    • ドメイン名: このフィールドは情報の重要な部分を表します。ディレクトリ テナント ドメイン名のテナントに固有の部分であり、すべてのディレクトリ テナントを相互に区別します。

      すべてのディレクトリ テナントは、作成時に、<tenantname>.onmicrosoft.com の形式のドメインによって識別されます。このドメインは、すべてのディレクトリ ユーザーの UPN、および一般にディレクトリ テナントを識別する必要があるすべての場所で、使用されます。作成した後で、所有する追加ドメインを登録できます。詳細については、「インターネットのドメイン」を参照してください。

      [ドメイン名] は一意でなければなりません。一意の値を選択するには UI 検証ロジックが役に立ちます。ユーザーとパートナーがディレクトリ テナントについてやり取りするときに役に立つので、会社を参照するハンドルを選択することをお勧めします。

    そのダイアログに入力したものすべてを使用してディレクトリ テナントが作成されます。右下にあるチェック ボタンをクリックするとすぐに、Azure AD は指定されたパラメーターに従って新しいテナントを作成します。新しいエントリを下で確認できます。



    エンタープライズ ディレクトリ

    noteメモ
    作成されたディレクトリ テナントは、クラウド内のユーザーと資格情報を格納するように構成されます。ディレクトリ テナントと Windows Server Active Directory のオンプレミスの配置を統合する必要がある場合は、こちらで詳細な説明を参照してください。

  3. 新しく作成されたディレクトリ エントリをクリックして、ユーザー管理 UI を表示します。



    すべてのテナント ユーザー

    ディレクトリ テナントは、新しいテナントが作成された Azure サブスクリプションを管理している Microsoft アカウントの場合を除いて、最初は空です。

    この Microsoft アカウントは、テナントのグローバル管理者の特権があることを確認するためにここに一覧されます。ただし、Azure Management Portal UI によって実行される操作のみを保持します。その Microsoft アカウントは Web SSO などのフローのためにディレクトリ テナントで実際に認証することはできないため、Web SSO チュートリアルのテスト ユーザーとしては使用できません。

    後の Web SSO シナリオを実行できるよう、新しいユーザーをディレクトリに追加します。

  4. 画面の下部にあるバーで [ユーザーの追加] コマンドをクリックします。次のようなダイアログが表示されます。



    ユーザーの追加

    [ユーザーの追加] ダイアログでは最初に、ディレクトリ ユーザーを作成するのか、または既存の Microsoft アカウントを追加するのかを指定します (サブスクリプションの管理に使用する場合、現在の Microsoft アカウントと同じ制限があります)。ワークフロー用のディレクトリ ユーザーが必要なので、[組織の新規ユーザー] エントリを選択します。

  5. ユーザー名を選択し、右下隅の矢印をクリックします。



    ユーザーの追加

    次の画面では、いくつかの基本ユーザー属性を選択できます。ユーザーに割り当てるロールにより、ユーザーがディレクトリにアクセスしたときに実行できることが決まります。

    アカウントの有効な電子メール アドレスを指定する必要があります。また、多要素認証のプレビューを試すオプションもあります。

  6. 値を入力した後、右下隅の矢印をクリックして次の画面に移動します。



    一時パスワード

    最後の手順では、管理ポータルによって一時的なパスワードが生成されます。初めてログインするときにはこのパスワードを使用する必要があります。その時点で、ユーザーはパスワードを変更する必要があります。シナリオのテスト用にすべてのコンポーネントを配置した後で必要になるので、一時パスワードをどこかに保存してください。

    以上で、Web SSO シナリオにおいて認証機関を提供するために必要なすべてのものが用意されました (ディレクトリ テナントと有効なユーザー)。

このセクションでは、Contoso 従業員の ToDo 項目を管理する ToDo リスト サービスを作成します。このサービスは、ASP.NET Web API で構成され、JSON Web トークン ハンドラーを使用して、承認されたユーザーとクライアントだけがサービスにアクセスできることを保証します。

  1. Visual Studio を開始し、[ファイル] の [新規作成] をポイントし、[プロジェクト] をクリックします。

  2. [新しいプロジェクト] ダイアログで、左側のメニューの Visual C# テンプレートから [Web] を選択し、ASP.NET MVC 4 Web Application テンプレートを選択します。新しいプロジェクトの名前を “ToDoListService” に、ソリューションの名前を “ToDoListApp” に設定して、[OK] をクリックします。

  3. [新しい ASP.NET MVC 4 プロジェクト] ダイアログが表示されます。Web API テンプレートを選択して、[OK] をクリックします。Web API テンプレートによって、簡単な Web API プロジェクトに必要なスキャフォールディングが作成されます。



    新しい ASP.NET MVC 4 プロジェクト

  4. 新しいプロジェクトが作成された後、F5 キーを押してプロジェクトを実行します。ブラウザーが開き、次のスクリーンショットのようなページが表示されます。



    結果として作成される新しい ASP.NET プロジェクト

    表示された Web ブラウザーで、Visual Studio によってアプリケーションに自動的に割り当てられる URL に注意してください。上の図の http://localhost:29062/ はその例です。この値は以下の構成値を表しており、後で使用する必要があります。

    • ToDo リスト サービス プロジェクトの対象ユーザー

    • Azure AD に登録されている App ID URI。ToDo リストによってサービスを識別するために使用されます。

    実稼働アプリケーションの場合、この値はアプリケーションをホストするサイトのベース アドレスとして推奨されます。例: https://todolistservice.azurewebsites.net。ただし、テナント内で一意であればどのような URI でもかまいません。

    Important重要
    実稼働サービスでは、SSL を使用してサービスに対する通信を保護し、認証トークンの盗難および特権昇格攻撃の実行を防ぐ必要があります。

  5. Web ブラウザーを閉じて、次の手順に進みます。

次に、必要な依存関係をプロジェクトに追加します。

  1. ソリューション エクスプローラーで [参照] を右クリックし、[参照の追加] をクリックします。

  2. [参照マネージャー] ダイアログで System.IdentityModel アセンブリを選択し、[OK] をクリックします。参照がプロジェクトに追加されます。

  3. [参照] フォルダーを右クリックして [NuGet パッケージの管理…] をクリックし、JSON Web トークン ハンドラーの NuGet パッケージをインストールします。

  4. [NuGet パッケージの管理] ダイアログで、"jwt handler" を見つけます。結果が表示されたら、[Microsoft .NET Framework 用 JSON Web トークン ハンドラー] を選択し、[インストール] をクリックします。

  5. [ライセンスの同意] ダイアログが表示されます。ライセンスの条件に同意する場合は、[同意する] をクリックします。JWT ハンドラーがダウンロードされて、プロジェクト内で参照されます。

  6. 同じ [NuGet パッケージの管理] ダイアログで、"token validation extension" を検索します。結果が表示されたら、[Microsoft .NET Framework 4.5 用 Microsoft トークン検証拡張機能] を選択し、[インストール] をクリックします。

  7. [ライセンスの同意] ダイアログが表示されます。ライセンスの条件に同意する場合は、[同意する] をクリックします。トークン検証拡張機能がダウンロードされて、プロジェクト内で参照されます。

これで、必須の依存関係を追加しました。Azure AD によって発行されたトークンを検証するために JWT ハンドラーの使用を開始するには、Global.asax.cs ファイルを更新する必要があります。クライアントが Web API サービスを呼び出すと、トークンが承認ヘッダーから読み取られます。

トークンが検証されて次の内容が確認されます。

  • トークン発行者は正しく、お使いの Azure AD テナントの発行者である

  • トークンが正しい x509 証明書で暗号署名されている

  • トークンが呼び出し元アプリケーションに必要な権限を付与している

更新された Global.asax.cs ファイルの完全な内容は次のとおりです。その次に変更の説明があります。

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

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

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
        }
    }

    internal class TokenValidationHandler : DelegatingHandler
    {
        // Domain name or Tenant name
        const string domainName = "[company/domain name like treyresearch.onmicrosoft.com]";
        const string audience = "[service App ID URI configured in tenant/domain for the Service App ex: http://localhost:40316/]";

        static DateTime _stsMetadataRetrievalTime= DateTime.MinValue;
        static List<X509SecurityToken> _signingTokens = null;
        static string _issuer = string.Empty;
        

        // SendAsync is used to validate incoming requests contain a valid access token, and sets the current user identity 
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jwtToken;
            string issuer;
            List<X509SecurityToken> signingTokens;

            if (!TryRetrieveToken(request, out jwtToken))
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }

            try
            {
                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
            {
                CertificateValidator = X509CertificateValidator.None
            };

            TokenValidationParameters validationParameters = new TokenValidationParameters
            {
                AllowedAudience = audience,
                ValidIssuer = issuer,
                SigningTokens = signingTokens
            };

            try
            {

                 // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
                                                validationParameters);

                //set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = claimsPrincipal;
                }            

                // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);
            }
            catch (SecurityTokenValidationException)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }
        }

        // Reads the token from the authorization header on the incoming request
        private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
        {
            token = null;
            string authzHeader;

            if (!request.Headers.Contains("Authorization"))
            {
                return false;
            }

            authzHeader = request.Headers.GetValues("Authorization").First<string>();

            // Verify Authorization header contains 'Bearer' scheme
            token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split(' ')[1] : null;

            if (null == token)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Parses the federation metadata document and gets issuer Name and Signing Certificates
        /// </summary>
        /// <param name="metadataAddress">URL of the Federation Metadata document</param>
        /// <param name="issuer">Issuer Name</param>
        /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
        static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
        {
            signingTokens = new List<X509SecurityToken>();

            // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
            if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
                || string.IsNullOrEmpty(_issuer)
                || _signingTokens == null)
            {
                MetadataSerializer serializer = new MetadataSerializer()
                {
                    CertificateValidationMode = X509CertificateValidationMode.None
                };
                MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                // get the issuer name
                if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                {
                    _issuer = entityDescriptor.EntityId.Id;
                }

                // get the signing certs
                _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                _stsMetadataRetrievalTime = DateTime.UtcNow;
            }

            issuer = _issuer;
            signingTokens = _signingTokens;
        }

        static List<X509SecurityToken> ReadSigningCertsFromMetadata(EntityDescriptor entityDescriptor)
        {
            List<X509SecurityToken> stsSigningTokens = new List<X509SecurityToken>();

            SecurityTokenServiceDescriptor stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();

            if (stsd != null && stsd.Keys != null)
            {
                IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
                                                             Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());

                stsSigningTokens.AddRange(x509DataClauses.Select(clause => new X509SecurityToken(new X509Certificate2(clause.GetX509RawData()))));
            }
            else
            {
                throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
            }

            return stsSigningTokens;
        }
    }
}


このコードを実行する前に、以下の定数を設定する必要があります。

  • domainName: これは、Azure AD テナントに登録したドメイン名です。例:contoso.onmicrosoft.com

  • audience: これは、後で Azure AD を使用して登録する ToDo リスト サービスのアプリ ID URI です。この定数の標準値は、ToDo リスト サービスが実行しているベース アドレスです。Visual Studio で開発するときの例: http://localhost:29062/

サービスの audience 値としてはベース アドレスを使用することをお勧めします。これは、呼び出し元がサービス アドレスからターゲット サービスの ID を取得できるためです。

上で示したコードについて詳しく説明します。

Global.asax.cs では、以下のためにトークン ハンドラーがパイプラインで構成されています。

  • 要求の承認ヘッダーからトークンを取得する

  • テナントのセキュリティ トークン サービスからメタデータを取得する

  • メタデータの署名キーと発行者を使用してトークンを検証する

  • 現在のスレッドの ID をトークンのユーザー ID に設定する

  • 呼び出し元アプリケーション (ToDo リスト クライアントなど) に、ユーザーに代わって Web API を呼び出すために必要な権限が付与されていることを確認する

以下の using ディレクティブが Global.asax.cs ファイルに追加されています。

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

次に、DelegatingHandler クラスから派生した TokenValidationHandler クラスが追加されています。TokenValidationHandler は、サービスへの認証に使用されるトークンを検証する作業のほとんどを実行します。

ハンドラーの作成を始めるには、次のコードを追加します。

internal class TokenValidationHandler : DelegatingHandler
    {
        // Domain name or Tenant name
        const string domainName = "[company/domain name like treyresearch.onmicrosoft.com]";
        const string audience = "[service App ID URI configured in tenant/domain for the Service App ex: http://localhost:40316/]";

        static DateTime _stsMetadataRetrievalTime= DateTime.MinValue;
        static List<X509SecurityToken> _signingTokens = null;
        static string _issuer = string.Empty;

domainName および audience を設定する必要があります。

domainName は、Azure AD にサインアップしているときに登録したドメイン名です。値の例: contoso.onmicrosoft.com

audience は ToDo リスト サービスのアプリ ID URI です。この値はサービスが実行されているローカル アドレスから派生します (例: http://localhost:29062/)。アプリ ID URI は、サービスに対する認証に使用されるトークンの audience クレームとして設定され、トークンが目的とするターゲットを示します。これにより ToDo リスト サービスでは、トークンが別のサービス向けでないことを検証することと、トークンを拒否することが可能になるので、転送攻撃の防止にも役立ちます。

_issuer フィールドおよび _signingTokens フィールドは設定されません。これらは、Azure AD によってホストされるフェデレーション メタデータ ドキュメントからの情報を使用し、後で設定されます。また、この値は今後動的に更新させることができます。アプリケーションがこの情報を更新できることが重要です。証明書のロールオーバーが発生すると時間の経過と共に情報が変わる可能性があるため、特に証明書の署名にとって重要です。

フェデレーション メタデータ ドキュメントを読み取り、発行者と証明書の情報を取得するために、次のコードを追加します。

        /// <summary>
        /// Parses the federation metadata document and gets issuer Name and Signing Certificates
        /// </summary>
        /// <param name="metadataAddress">URL of the Federation Metadata document</param>
        /// <param name="issuer">Issuer Name</param>
        /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
        static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
        {
            signingTokens = new List<X509SecurityToken>();

            // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
            if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
                || string.IsNullOrEmpty(_issuer)
                || _signingTokens == null)
            {
                MetadataSerializer serializer = new MetadataSerializer()
                {
                    // turning off certificate validation for demo. Don't use this in production code.
                    CertificateValidationMode = X509CertificateValidationMode.None
                };
                MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                // get the issuer name
                if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                {
                    _issuer = entityDescriptor.EntityId.Id;
                }

                // get the signing certs
                _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                _stsMetadataRetrievalTime = DateTime.UtcNow;
            }

            issuer = _issuer;
            signingTokens = _signingTokens;
        }

フェデレーション メタデータ ファイルの URL が指定されると、このコードはファイルの内容を読み取り、要求された情報を返します。

フェデレーション メタデータの読み取りは (すべての要求についてではなく) 定期的に行うだけでよいので、単純な形式のメタデータ キャッシュが実装されています。メタデータの取得は、初期使用時または最後の読み取りから 24 時間後に行われます。フェデレーション メタデータに対する最も重要な更新は、署名証明書のロールオーバーが発生する場合に行われます。ロールオーバー中の数日間は、期限切れになる証明書と新しい証明書の双方を使用できます。この延長期間があることで、両方の証明書を取得するのに十分な時間がサービスで確保されます。

次に、TokenValidationHandler クラスに配置されている SendAsync() メソッドについて説明します。

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

SendAsync() は、ToDo リスト サービスに対する要求があるたびに呼び出されます。このメソッドは、すべての受信要求を認証するために要求に有効なトークンが含まれていることを確認したうえで、認証済みユーザーの現在のユーザー ID を設定します。次のコードは、ToDo リスト サービスに対して行われている要求の承認ヘッダーからトークンを読み取ります。

if (!TryRetrieveToken(request, out jwtToken))
{
    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}

上記のコードでは TryRetieveToken() を使用しています。このメソッドは、次のコードに示すとおり、ヘッダーからトークンを読み取ります。

// Reads the token from the authorization header on the incoming request
        private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
        {
            token = null;
            string authzHeader;

            if (!request.Headers.Contains("Authorization"))
            {
                return false;
            }

            authzHeader = request.Headers.GetValues("Authorization").First<string>();

            // Verify Authorization header contains 'Bearer' scheme
            token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split(' ')[1] : null;

            if (null == token)
            {
                return false;
            }

            return true;
        }

上記のコードは、承認ヘッダー文字列にトークンが含まれているかどうかを確認します。ヘッダーの値は次の規格に基づいて "Bearer" で始まる必要があります。RFC 6750 "OAuth 2.0 Authorization Framework:Bearer Token Usage”。ここに、Web API 呼び出しでトークンを渡す方法が説明されています。ヘッダー内にトークンが見つかった場合、true が返されます。

ヘッダーから取得したトークンは、検証する必要があります。検証プロセスでは、Azure AD テナント メタデータ、発行者、および対象ユーザーから取得した資格情報に対して署名トークンを検証する必要があります。この検証は JWT ハンドラーが TokenValidationParameters オブジェクトに設定された値に基づいて行います。このオブジェクトは、次に示すとおり、SendAsync() メソッドで作成されます。

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jwtToken;
            string issuer;
            List<X509SecurityToken> signingTokens;

            if (!TryRetrieveToken(request, out jwtToken))
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }

            try
            {
                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
            {
                CertificateValidator = X509CertificateValidator.None
            };

            TokenValidationParameters validationParameters = new TokenValidationParameters
            {
                AllowedAudience = audience,
                ValidIssuer = issuer,
                SigningTokens = signingTokens
            };

            try
            {

                 // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
                                                validationParameters);

                //set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = claimsPrincipal;
                }            

                // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);
            }
            catch (SecurityTokenValidationException)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }
        }

トークンの検証に成功すると、トークンのクレームを使用して呼び出し元の ID が現在のスレッドおよび HttpContext に設定されます (どちらも、ASP.NET と IIS の間での移行中にコンテキストが失われないようにするために必要です)。クレームには、ユーザーに関する情報と、ToDo リスト クライアントである呼び出し元に関する情報です。

トークンの検証に失敗した場合は、HTTP Unauthorized エラーが返されます。最後に、メソッドから制御が戻ると、トークンに含まれていた権限が確認されます。これには、現在の要求プリンシパルの "scp" (scope) クレームを確認します。必須の値は、呼び出し元に ToDo 項目の作成および読み取りを許可する "user_impersonation" です。

                 // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("scp").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);

クライアント アプリをテナントに追加することにテナント管理者が同意すると、ToDo リスト クライアントに付与される権限が構成されます。これについては後半の手順で扱います。

TokenValidationHandler のしくみを理解したところで、これを Application_Start() メソッドに登録する必要があります。

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
            GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
        }

上記スニペットの最終行では、受信要求ごとにハンドラーを要求パイプラインに追加しています。

次に、ToDo 項目のモデルを追加します。このモデルは、ToDo 項目のデータを含む小さなクラスになります。

  1. ソリューション エクスプローラーで [Models] フォルダーを右クリックし、[追加] をクリックします。次に、[クラス] をクリックします。



    新しいクラスの追加

  2. [新しい項目の追加] ダイアログで、名前として「ToDoItem」と入力し、[追加] をクリックします。



    新しいクラスの追加

  3. 新しいクラスがプロジェクトに追加されました。既定のコードを次のコードに置き換えます。

    namespace ToDoListService.Models
    {
        public class ToDoItem
        {
            public string Title { get; set; }
            public string Owner { get; set; }
        }
    }
    
    

  1. ソリューション エクスプローラーで、[Controllers] フォルダーを右クリックし、[追加] をクリックします。次に、[コントローラー] をクリックします。



    新しいコントローラーの追加

  2. [コントローラーの追加] ダイアログで、名前に「ToDoListController」と入力します。[テンプレート] ドロップダウン メニューの [空の読み取り/書き込み操作のある API コントローラー] を選択し、[追加] をクリックします。



    コントローラーの追加

  3. ToDoListController.cs を開き、既存のコードを次のコードに置き換えます。

    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Web.Http;
    using ToDoListService.Models;
    
    namespace ToDoListService.Controllers
    {
        public class ToDoListController : ApiController
        {
            // ToDo items list for all users
            static ConcurrentBag<ToDoItem> todoBag = new ConcurrentBag<ToDoItem>();
    
            // GET api/todolist returns 
            public IEnumerable<ToDoItem> Get()
            {
                Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
    
                return from todo in todoBag
                       where todo.Owner == subject.Value
                       select todo;
            }
    
            // POST api/todolist
            public void Post(ToDoItem todo)
            {
                if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
                {
                    todoBag.Add(new ToDoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
                }
            }
        }
    }
    
    では、追加したコードについて説明します。ConcurrentBag オブジェクトは、すべてのユーザーの ToDo 項目を保存するためのスレッドセーフ オブジェクトです。実際のサービスにおいてこれらの項目はデータベースに保存されますが、便宜上、ここではデータをメモリに格納するだけです。

    // Todo items list for all users
    static ConcurrentBag<ToDoItem> todoBag = new ConcurrentBag<ToDoItem>();
    
    ToDo 項目を格納する場所を確保したところで、Post() メソッドを更新して、項目を ConcurrentBag に追加する機能を含めます。

    // POST api/todolist
    public void Post(ToDoItem todo)
    {
        if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
        {
            todoBag.Add(new ToDoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
        }
    }
    
    ToDoItem オブジェクトを受け入れるようにメソッド シグネチャが更新されています。これにより、呼び出し元からオブジェクトまでのフォーム POST がシリアル化されます。さらに、空のタイトルを持つ ToDoItem が追加されないようにしています。

    新しい ToDoItem が作成され、ToDo リストに保存されます。項目の所有者は、ユーザーにとって一意であることが保証されているサブジェクト識別子に設定されます。このメソッドの ClaimsPrincipal は、あらかじめ Global.asax.cs に設定されており、Azure AD からのトークンにあるユーザーの ID を反映します。結果的に、ユーザーは自身の ToDo 項目にしかアクセスできなくなります。

    オブジェクト ID (ユーザー ID を検証するために使用されている oid クレームの種類) は、Azure AD のユーザー アカウントのオブジェクト ID です。

    次に、Get() メソッドについて説明します。

            // GET api/todolist returns 
            public IEnumerable<ToDoItem> Get()
            {
                Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
    
                return from todo in todoBag
                       where todo.Owner == subject.Value
                       select todo;
            }
    
    
    IEnumerable<ToDoItem> を返すようにシグネチャが更新され、このオブジェクトは現在のユーザーが所有する ToDo 項目に設定されます。

    現在のユーザーの ID を確認するため、現在の ClaimsPrincipal を参照し、次に、ユーザーが所有する項目だけを返します。

  4. F5 キーを押して、サービスが実行されることを確認します。

以上の手順で Web API ができたので、次にクライアント アプリケーションを作成します。このアプリケーションは、ユーザーが ToDo 項目を表示し、新しい項目を追加する機能を持つ ToDo リスト クライアントです。

クライアントを作成する前に、Azure AD テナントでクライアントとサービスを登録します。これは、ToDo リスト クライアントに使用される Azure AD のクライアント ID を取得するために必要です。

次の図は、ここで構築するアプリケーションです。このアプリケーションには、ユーザーが、サービスを呼び出し、タスクを追加または表示する権限を ToDo リスト クライアントに付与する機能があります。

作業一覧アプリケーション

アプリケーションを構築する手順は次のとおりです。

  1. Windows ストア アプリケーションのプロジェクトを作成する

  2. XAML でユーザー インターフェイスを作成する

  3. 分離コードで AAL を使用して Web API に対して認証する

  4. 他のユーザーが ToDo 項目を表示できるように、アプリケーションに関連付けられたアカウントを削除するオプションを追加する

Windows 8 開発者用ライセンスを持っていない場合は、アプリケーションの作成を始める前に取得する必要があります。開発者用ライセンスは無料ライセンスです。こちらの手順に従って取得することができます。

  1. Visual Studio で、既存のソリューションに新しいプロジェクトを作成します。ソリューション エクスプローラーでソリューションを右クリックし、[追加] をクリックし、[新しいプロジェクト] をクリックします。

  2. [新しいプロジェクトの追加] ダイアログの左側にある [Visual C#] ドロップダウンから [Windows ストア] を選択し、[新しいアプリケーション (XAML)] を選択します。



    新しいプロジェクトの追加



    新しいプロジェクトの追加

  3. プロジェクトに「TodoListClient」と名前を付けて、[OK] をクリックします。

ToDo リスト クライアント プロジェクトを使用するには、Azure Authentication Library (AAL) for Windows Store NuGet Package をインストールする必要があります。NuGet Package Manager を開き、オンラインで「AAL」を検索し、[AAL for Windows Store Package] を選択します。

ToDo リスト クライアントで AAL が動作するには、次の機能をアプリケーションに付与する必要があります。

  • エンタープライズ認証

  • インターネット (クライアント)

  • プライベート ネットワーク (クライアントとサーバー)

  • 共有ユーザー証明書

これらの情報は、Package.appxmanifest ファイルを開いて設定できます。

Package.appxmanifest ファイルを開く

[機能] タブに移動し、必要な機能を選択します。

必要な機能の選択

ToDo リスト クライアントでトークンを取得する AAL を呼び出すときに、ユーザーが AAD に対して認証し、サービスを呼び出すアクセス権を ToDo リスト クライアントに付与できるようにするために、場合によっては AAL で Windows Web Authentication Broker を開く必要があります。Web Authentication Broker は、Windows ストア アプリケーションにサインイン Web ページを表示するために使用されます。

Web Authentication Broker は、ブローカーでホストされている Web ページが、ブローカーに登録されているコールバック URI に移動したときに閉じます。既定のコールバック URI は、WebAuthenticationBroker.GetCurrentApplicationCallbackUri() を呼び出すと検索することができます。URI にはアプリケーションの SID が含まれており、次のようになります。ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/

これは、Azure AD が ToDo リスト クライアントからの OAuth 2.0 要求に応答するために使用する URI になります。そのため、間違ったアプリケーションに応答を返さないように Azure AD に登録する必要があります。

  1. MainPage.xaml.cs ファイルに using Windows.Security.Authentication.Web; という using ディレクティブを追加します。

  2. 次のように、MainPage.xaml.csOnNavigatedTo() メソッドにコードを追加します。

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        string redirectUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri().ToString();
    }
    
  3. 追加したこの行にブレークポイントを設定し、ToDo リスト クライアント プロジェクトを実行します。



    OnNavigatedTo メソッド

  4. ブレークポイントにヒットすると、次の行をステップ実行し (F11)、redirectUri の値を保存します。これは ToDo リスト クライアントのリダイレクト URI です。クライアントを Azure AD に登録するために必要になります。

    noteメモ
    ToDo リスト クライアント アプリケーション URL は、Azure AD でアプリケーションのサービス プリンシパルを適切に構成するために必要です。例:ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/

この段階で、ToDo リスト Web API プロジェクトと ToDo リスト クライアントを作成しました。これらは Azure Management Portal を使用して、Azure AD に登録される必要があります。この登録プロセスの結果として、ToDo リスト クライアントはクライアントのユーザーに代わって ToDo リスト Web API を呼び出すことを許可されます。より深いレベルで、クライアントのユーザーが Azure AD で認証した後であるため、クライアントは Web API を呼び出すことを許可されます。また、user_impersonation 権限の scp クレームを含むトークンが返されます。

  1. Web ブラウザーで、https://manage.windowsazure.com にアクセスします。

  2. Azure Management Portal で、左側メニューの [Active Directory] アイコンをクリックして、お客様の組織の名前をクリックします。

  3. [クイック スタート] メニューの [アプリケーション] タブをクリックします。

  4. コマンド バーの [追加] をクリックします。

  5. ウィザードの先頭ページで、Web API のユーザー フレンドリ名を [名前] フィールドに入力します。この名前は、Azure AD ディレクトリに登録されたクライアント アプリケーションを表示するときに容易に識別できるようになります。そのため、今回の場合は ToDo List Web API という名前にします。[種類] の下の [Web アプリケーションまたは Web API] が既に選択されているので、矢印をクリックして次のページに進みます。

  6. 次のページで、Web API について [アプリの URL] および [アプリ ID URI] に情報を入力します。アプリの URL は Web API のアドレス、アプリ ID URI はアプリの論理識別子です。このサンプルでは、これらの値はどちらも同じなので、http://localhost:29062/ に設定し、次へ進む矢印をクリックします。

  7. 次のページで、Web API で必要とされるディレクトリ アクセスの種類を選択します。このサンプルでは Graph API にアクセスしないので、[シングル サインオン] を選択する必要があります。次に、チェックマーク ボタンをクリックして作業を完了します。

    Web API を追加した後は、クライアント アプリケーションを登録する必要があります。次の手順は、このプロセスの実行方法を示します。

  8. Azure Management Portal で、お客様の組織のディレクトリの [アプリケーション] タブに戻ります。

  9. コマンド バーの [追加] をクリックします。

  10. ウィザードの最初のページで、クライアント アプリケーションのユーザー フレンドリ名 (ToDo List Client など) を入力し、[種類] で [ネイティブ クライアント アプリケーション] オプションを選択します。矢印をクリックして次のページに進みます。

  11. 次のページで、クライアント アプリケーションのリダイレクト URI を入力します。OAuth 2.0 要求に応答して Azure AD がリダイレクトする場所です。これは、前のセクションで取得した ms-app://… 値を使用する場所です (ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/ など)。チェックマーク ボタンをクリックします。

    Azure AD に、クライアント アプリケーションが追加されました。次の手順は、クライアント アプリケーションから Web API へのアクセスの構成方法を示します。

  12. クライアント アプリケーションの [クイック スタート] ページが表示されます。トップ メニューの [構成] をクリックすると、アプリケーションの構成ページが表示されます。

  13. 構成ページの [Web API] セクションで、[このネイティブ クライアント アプリケーションの Web API アクセスを構成する] というドロップダウン メニューを展開し、前に追加した [ToDo リスト Web API] をクリックします。

  14. サービスを選択したら、コマンド バーの [保存] をクリックします。

ToDo リスト クライアントの作成を続けます。

作成しているクライアントの最初の部分は、ToDo リスト クライアントのユーザー インターフェイスです。

MainPage.xaml.cs ファイルを開き、既定のマークアップを次の内容で置き換えます。

<Page
    x:Class="ToDoListClient.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ToDoListClient"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="120"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Column="1" Grid.Row="1" Style="{StaticResource PageHeaderTextStyle}" Text="ToDo List"/>
        <Grid Grid.Column="1" Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="300" />
            </Grid.ColumnDefinitions>
            <TextBox x:Name="ToDoText" Grid.Column="0" Grid.Row="2"  Margin="5" />
            <Button Grid.Column="1" Margin="10,0,0,0" Click="Button_Click_Add_Todo" >Add ToDo Item</Button>
            <HyperlinkButton Grid.Column="2" HorizontalAlignment="Right" Content="Remove Account" Click="HyperlinkButton_Click_Remove_Account"/>
        </Grid>
        <GridView x:Name="ToDoList" Grid.Column="1" Grid.Row="3" Margin="0,10,0,0" >
            <GridView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Width="200" Height="150" Margin="10" Background="#FFA2A2A4" >
                        <TextBlock Text="{Binding Title}" FontSize="24" TextWrapping="Wrap" Margin="10"/>
                    </StackPanel>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>
</Page>

これで、3 列と 4 行のグリッド内のシンプルなグリッド レイアウトができます。

  • <TextBox> は、ToDo 項目のタイトルを入力する場所です。

  • <Button> は、ToDo 項目の送信に使用されます。

  • <HyperlinkButton> は、ユーザー セッション データの削除に使用されます。

  • <GridView> は、ユーザーのすべての ToDo 項目を表示するために使用されます。分離コード ファイルは、ToDo リストを GridView の背後にデータを配置します。

以上で UI を作成するマークアップは完成です。次に、分離コード ファイルを追加します。

次に分離コード ファイルを追加して、Web API を使用して ToDo 項目の追加と取得を実行できるようにします。クライアント コードは、サービスにアクセスするために Azure AD から認証トークンを要求するために AAL を使用します。

MainPage.xaml.cs ファイルを開き、以下のコードを追加します。

using Microsoft.Preview.WindowsAzure.ActiveDirectory.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Windows.Data.Json;
using Windows.Security.Authentication.Web;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace TodoListClient
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        //Tenant name
        const string domainName = "[Your Domain Name. Example: contoso.onmicrosoft.com]";

        //Client app information
        const string clientID = "[Your client id. Example: de119d9a-c0b9-4ac0-b794-ca82e9d7dcd4]";

        //Resource information
        const string resourceAppIDUri = "[Your service application Uri. Example: http://localhost:29062/]";
const string resourceBaseAddress = "[The base address at which your service can be reached. Example: http://localhost:29062/]";

        private HttpClient httpClient = new HttpClient();

        private AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/" + domainName);

        public MainPage()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            GetTodoList();
        }

        // Get the current user's todo items
        private async void GetTodoList()
        {
            HttpResponseMessage response;

            // Acquire a token from AAL. AAL will cache the authorization state in a persistent cache.
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Call the todolist web api
            response = await httpClient.GetAsync(resourceBaseAddress + "/api/todolist");

            // Verify the call to the todo list service succeeded
            if (response.IsSuccessStatusCode)
            {
                // Read the response as a Json Array and databind to the GridView to display todo items
                var todoArray = JsonArray.Parse(await response.Content.ReadAsStringAsync());

                TodoList.ItemsSource = from todo in todoArray
                                       select new
                                       {
                                           Title = todo.GetObject()["Title"].GetString()
                                       };
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

        // Add a new todo item
        private async void Button_Click_Add_Todo(object sender, RoutedEventArgs e)
        {
            // Acquire a token from AAL. AAL will cache the authorization state
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Forms encode todo item, to POST to the todo list web api
            HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("Title", TodoText.Text) });

            // Call the todolist web api
            var response = await httpClient.PostAsync(resourceBaseAddress + "/api/todolist", content);

            if (response.IsSuccessStatusCode)
            {
                TodoText.Text = "";
                GetTodoList();
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

        // Clear the authorization state in the application
        private void HyperlinkButton_Click_Remove_Account(object sender, RoutedEventArgs e)
        {
            // Clear session state from the token cache
            (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();

            // Reset UI elements
            TodoList.ItemsSource = null;
            TodoText.Text = "";
        }

        // Displays appropriate error when acquiring a token fails
        private async void DisplayErrorWhenAcquireTokenFails(AuthenticationResult result)
        {
            MessageDialog dialog;

            switch (result.Error)
            {
                case "authentication_canceled":
                    // User cancelled, no need to display a message
                    break;
                case "temporarily_unavailable":
                case "server_error":
                    dialog = new MessageDialog("Please retry the operation. If the error continues, please contact your administrator.", "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
                default:
                    // An error occurred when acquiring a token, show the error description in a MessageDialog 
                    dialog = new MessageDialog(string.Format("If the error continues, please contact your administrator.\n\nError: {0}\n\nError Description:\n\n{1}", result.Error, result.ErrorDescription), "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

        // Displays appropriate error when calling a resource service fails
        private async void DisplayErrorWhenCallingResourceServiceFails(HttpStatusCode statuscode)
        {
            MessageDialog dialog;

            switch (statuscode)
            {
                case HttpStatusCode.Unauthorized:

                    // An unauthorized error occurred, indicating the security token provided did not satisfy the service requirements
                    // acquiring a new token may fix the issue, this requires clearing state in the AAL cache.
                    // 
                    dialog = new MessageDialog("Would you like to reset your connection? This may help fix the problem.", "Sorry, accessing your todo list has hit a problem.");

                    dialog.Commands.Add(new UICommand("Yes", (c) =>
                    {
                        (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();
                        GetTodoList();
                    }));

                    dialog.Commands.Add(new UICommand("No"));

                    await dialog.ShowAsync();
                    break;

                default:
                    dialog = new MessageDialog("If the error continues, please contact your administrator.”, “Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }
    }
}
noteメモ
上のコードを実行する前に、次の 3 つのフィールドを設定する必要があります。

  1. domainName: Azure AD テナントのドメイン名 (contoso.onmicrosoft.com など)

  2. clientID:Azure AD にクライアントを登録したときに作成したクライアント ID (3b616fda-41f9-47cc-8f88-35481e93c2dc など)。この値は、Azure の管理ポータルのアプリケーションの構成ページから取得できます。

  3. resourceAppIDUri:ToDo リスト Web API のアプリ ID URI。標準的な方法では、サービスのベース アドレスと同じ値に設定します (http://localhost:29062/ など)。

  4. resourceBaseAddress:ToDo リスト Web API のベースアドレス。この値は、サービスがホストされているベース アドレスと一致する必要があります (http://localhost:29062/ など)。

次に、分離コードに追加するコードについて説明します。最初に、次の using ディレクトリが MainPage.xaml.cs の最初に追加されました。

using Microsoft.Preview.WindowsAzure.ActiveDirectory.Authentication;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using Windows.Data.Json;
using Windows.Security.Authentication.Web;
using Windows.UI.Popups;

次に、ToDo リスト クライアントと ToDo リスト サービスのソリューション固有の値が追加されました。これらの値については、前のセクションを参照してください。

public sealed partial class MainPage : Page
{
    //Domain Name
    const string domainName = "[Your Domain Name. Example: contoso.onmicrosoft.com]";

    //Client app information
    const string clientID = "[Your client id. Example: de119d9a-c0b9-4ac0-b794-ca82e9d7dcd4]";

    //Resource information
    const string resourceAppIDUri = "[Your service application URI. Example: http://localhost:29062/]";
    const string resourceBaseAddress = "[The base address at which your service can be reached. Example: http://localhost:29062/]";

HttpClient のプライベート インスタンス フィールドの private HttpClient httpClient; が、ToDo リスト サービスに対する呼び出しを実行するために追加されました。

次に AuthenticationContext にもう 1 つのプライベート インスタンスが追加されました。これは AAL クラスです。このクラスはテナント ID を使用して構成されています。これによって、正しい Azure AD テナントの承認を要求するように AAL に指示します。private AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/" + domainName);

次に、GetToDoList() メソッドについて説明します。このメソッドは、ToDo 項目を取得するために ToDo リスト サービスに対して呼び出しを実行します。サービスに対して認証するために、AAL AuthenticationContext クラスを使用してトークンを要求する呼び出しがあります。

ToDo リスト サービスのアプリケーション ID URI は最初のパラメーターとして渡されます。2 つ目のパラメーターは ToDo リスト クライアントの clientID です。この ID で、トークン要求内の発信元を識別します。

これは非同期呼び出しなので、呼び出しが返されるまでスレッドをブロックするには、await キーワードを使用します。この場合、GetTodoList() メソッドで async 修飾子を使用する必要があります。

        // Get the current user's todo items
        private async void GetTodoList()
        {
            HttpResponseMessage response;

            // Acquire a token from AAL. AAL will cache the authorization state in a persistent cache.
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

AcquireTokenAsync() を呼び出すと、結果が異なる場合があります。初めて使用する場合、この呼び出しにより Windows Web Authentication Broker が開き、Azure AD サインイン ページが表示されます。これを次に示します。

組織アカウントへのログイン

ユーザーがサインインした後は、Azure AD に対して認証されたユーザーの代理でサービスを呼び出すアクセス権が ToDo リスト クライアントに付与されます。この結果、更新トークンがクライアントから Azure AD に返されます。

AAL ライブラリは、Azure AD から返された更新トークンをキャッシュします。更新トークンがキャッシュ内にある場合、AcquireToken() の呼び出しで、ユーザーにトークンの入力を求めるプロンプトを再表示する必要がなくなります。

AAL が更新トークンを使用してクライアントのアクセス トークンを要求すると、サービスに対してクライアントを認証するために発行されるセキュリティ トークンに含まれるユーザーとクライアントの ID はエンコードされます。これにより、サービスでは両方の ID で呼び出しを承認できるようになります。

AcquireToken() の呼び出しが返されたら、結果で呼び出しが成功したことを確認します。

    // Verify that an access token was successfully acquired
    if (AuthenticationStatus.Succeeded != result.Status)
    {
    DisplayErrorWhenAcquireTokenFails(result);
    return;
    }

呼び出しが失敗した場合は、呼び出しで返された AuthorizationStatus を使用するエラーを表示する呼び出しがあります。DisplayErrorWhenAcquireTokenFails() を使用してエラーのチェックを実行し、必要に応じてユーザーにメッセージを表示します。このメソッドの本文を次に示します。

        // Displays appropriate error when acquiring a token fails
        private async void DisplayErrorWhenAcquireTokenFails(AuthenticationResult result)
        {
            MessageDialog dialog;

            switch (result.Error)
            {
                case "authentication_canceled":
// User cancelled, no need to display a message
                    break;
                case "temporarily_unavailable":
                case "server_error":
                    dialog = new MessageDialog("Please retry the operation. If the error continues, please contact your administrator.", "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
                default:
                    // An error occurred when acquiring a token, show the error description in a MessageDialog 
                    dialog = new MessageDialog(string.Format("If the error continues, please contact your administrator.\n\nError: {0}\n\n Error Description: {1}",result.Error, result.ErrorDescription), "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

処理するエラーのカテゴリは 3 つです。

  • ユーザーによるキャンセル:ユーザーが認証ダイアログ以外で取り消した場合は "authentication_canceled" エラーが返されます。ユーザーがこの操作を開始したため、ユーザーにメッセージを表示する必要はありません。

  • 再試行可能なエラー:"temporarily_unavailable" または "server_error" が返される場合、高負荷などの原因で Azure AD サービスに一時的な問題が発生した可能性があります。ユーザーはこの操作を再試行することができます。

  • その他:その他すべてのエラーです。通常は構成の問題が原因であり、ほとんどの場合、操作を再試行しても問題を修正できません。

次に、GetTodoList() メソッドに戻ります。エラーが発生しなかったとすると、AAL、AAL キャッシュ以外、または Azure AD の新しいトークンからトークンが返されます。次に、トークンを使用して、要求の承認ヘッダーを ToDo リスト サービスに設定します。承認ヘッダーを設定すると、RFC 6750「The OAuth 2.0 Authorization Framework:Bearer Token Usage」の規定 (OAuth ベアラ トークンを使用してリソースにアクセスする方法が定義されている) に従って、トークンの先頭には "Bearer" が付きます。

// Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
// the resource service.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

ToDo リスト サービス Web API に対して HTTP GET を実行し、ユーザーの ToDo 項目の一覧を取得します。

// Call the todolist web api
response = await httpClient.GetAsync(resourceBaseAddress + "/api/todolist");

呼び出しが成功した場合、Linq クエリを使用して、ToDo リスト サービスから返された JSON 配列で、UI の GridView オブジェクトのデータのソースとして使用できるコレクションを構築します。これで、データ バインドを使用して GridView が自動的に設定されます。MainPage.xaml に定義されている GridView ItemTemplate は、サービスから返される ToDo 項目の Title プロパティにテキスト ボックスをバインドします。

// Verify the call to the ToDo List Service succeeded
if (response.IsSuccessStatusCode)
{
    // Read the response as a Json Array and databind to the GridView to display todo items
    var todoArray = JsonArray.Parse(await response.Content.ReadAsStringAsync());

    TodoList.ItemsSource = from todo in todoArray
                                       select new
                                       {
                                           Title = todo.GetObject()["Title"].GetString()
                                       };
}
else
{
    DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
}

Web API の呼び出しが失敗した場合、ユーザーにメッセージを表示する DisplayErrorWhenCallingResourceServiceFails() が呼び出されます。エラー処理で、未承認のエラーが返される特殊な場合に対処します。ToDo リスト サービスは、サービスへのアクセスに使用するトークンが拒否されたときにこのエラーを処理します。たとえば、トークンの有効期限が切れた場合 (ただし、AAL はトークンの期限切れ前に新しいトークンを要求するため、この問題が発生することはほとんどありません) や、何らかの理由でトークンが検証チェックに合格しない場合などです。この場合、コードで AAL キャッシュをクリアして、トークンの取得を再開するオプションをユーザーに表示します。

呼び出されるサービスの動作に基づいて、その他の問題も発生することがあります。

        // Displays appropriate error when calling a resource service fails
        private async void DisplayErrorWhenCallingResourceServiceFails(HttpStatusCode statuscode)
        {
            MessageDialog dialog;

            switch (statuscode)
            {
                case HttpStatusCode.Unauthorized:

                    // An unauthorized error occurred, indicating the security token provided did not satisfy the service requirements
                    // acquiring a new token may fix the issue, this requires clearing state in the AAL cache.
                    // 
                    dialog = new MessageDialog("Would you like to reset your connection? This may help fix the problem.", "Sorry, accessing your todo list has hit a problem.");

                    dialog.Commands.Add(new UICommand("Yes", (c) =>
                    {
                        (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();
                        GetTodoList();
                    }));

                    dialog.Commands.Add(new UICommand("No"));

                    await dialog.ShowAsync();
                    break;

                default:
                    dialog = new MessageDialog("If the error continues, please contact your administrator.”, “Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

次に、新しい ToDo 項目をユーザーの ToDo リストに追加するコードについて説明します。このメソッドは、GetTodoList() メソッドの呼び出しと似ています。トークンは、AAL の AuthenticationContext クラスを使用して取得します。AAL が更新トークンを既にキャッシュしている場合、UI のプロンプトは表示されません。このトークンは ToDo リスト サービスに対する認証に使用されます。また、ToDo 項目を含むメッセージがエンコードされたフォームを使用して、POST 要求が ToDo リスト サービスに送信されます。ToDo 項目は、サービスでユーザーの ToDo リストに追加されます。

        // Add a new todo item
        private async void Button_Click_Add_Todo(object sender, RoutedEventArgs e)
        {
            // Aquire a token from AAL. AAL will cache the authorization state
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Forms encode todo item, to POST to the todolist web api
            HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("Title", TodoText.Text) });

            // Call the todolist web api
            var response = await httpClient.PostAsync(resourceBaseAddress + "/api/todolist", content);

            if (response.IsSuccessStatusCode)
            {
                TodoText.Text = "";
                GetTodoList();
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

このメソッドは、UI の [Add ToDo] ボタンのクリック イベントで呼び出されます。

<Button Grid.Column="1" Margin="10,0,0,0" Click="Button_Click_Add_Todo" >Add ToDo</Button>

以上で、ユーザーは、ToDo リスト サービスの呼び出し、項目の表示、項目のリストへの追加を行うアクセス権を ToDo リスト クライアントに付与できるようになりました。次に、クライアントから更新トークンをクリアするコードについて説明します。このコードでは、AAL キャッシュをクリアし、別のユーザーが ToDo リスト クライアントを使用できるようにします。トークン キャッシュはカスタムの実装に合わせて設定できます。既定では、DefaultTokenCache というキャッシュに設定されています。

  // Clear the authorization state in the application
private void HyperlinkButton_Click_Remove_Account(object sender, RoutedEventArgs e)
{
    // Clear session state from the token cache
    (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();

    // Reset UI elements
    TodoList.ItemsSource = null;
    TodoText.Text = "";
}

DefaultTokenCache は AAL の一部として提供され、AuthenticationContext で使用される既定のトークン キャッシュになります。DefaultTokenCache は、Azure AAD テナント、リソース、およびクライアントに基づいてトークンをキャッシュします。そのため、キャッシュ内に更新トークンとアクセス トークンが複数存在する可能性があります。

DefaultTokenCache が、Microsoft アカウントに接続する Windows 8 デバイスで実行されるアプリケーションに使用される場合、キャッシュのコンテンツは、同じアカウントに接続されている複数のコンピューター間でローミングされます。

上記のコードでは AuthenticationContext TokenCache にアクセスし、すべての承認状態をクリアします。このイベント ハンドラーは、UI でクリック イベントに関連付ける必要があります。

<HyperlinkButton Grid.Column="2" HorizontalAlignment="Right" Content="Remove Account" Click="HyperlinkButton_Click_Remove_Account"/>

以上で ToDo リスト クライアントのコードは完成です。サービスとクライアントをテストすることができます。

次に ToDo リスト クライアントをテストしてみましょう。クライアントとサービスの両方が実行されていることを確認します。クライアントが起動すると、次のページが表示されます。AAL を初めて呼び出したときには Web Authentication Broker が開き、ユーザーは Azure AD サインイン ページに資格情報を入力します。

組織アカウントへのログイン

ユーザーはタイトルを入力し、[Add ToDo Item] ボタンをクリックして項目を ToDo リストに入力できます。

作業一覧アプリケーションの初回実行

ボタンをクリックすると、AAL が再び呼び出されますが、今度はキャッシュ済みのトークンが返され、ToDo リスト サービスの呼び出しに使用されます。項目が追加され、ユーザーの ToDo 項目のリストが返されます。

作業一覧アプリケーションの初回実行

別のユーザーとして ToDo クライアントを試すには、Azure AD テナントにサインインします。Azure サブスクリプション内にディレクトリを作成した場合、Azure の管理ポータルに移動し、テナント管理者アカウントを使用してサインインします。

[Active Directory] タブでディレクトリを選択します。

下部のツール バーにある [ユーザーの追加] を選択して新しいユーザーを作成します。

ユーザーの追加

ユーザーの作成手順を示すダイアログが表示されます。

新しいユーザーの追加

アカウントを追加したら、ToDo リスト クライアントに戻り、[Remove Account] リンクをクリックします。これでクライアントからユーザーの状態がクリアされます。

[Add ToDo Item] ボタンをクリックし、AAL を呼び出すと、認証を求めるプロンプトがユーザーに表示されます。今回は、新しいユーザー アカウントを使用します。

アプリへのログイン

認証が完了すると、新しいユーザーの ToDo リストを管理できます。

完成したアプリ

ユーザー アカウントを切り替えると、サービスが各ユーザーに正しい項目を返していることをテストできます。

以上で ToDo リスト アプリケーションの構築は完了です。

このオプションのセクションでは、ToDo リスト サービスを Azure Web サイトに展開するために必要な手順について説明します。Azure Web サイトを作成したことがない場合は、入門としてこちらの記事を参照してください。Web サイトの URL を指定済みの場合は、この手順を終了し、サービスの対象ユーザーを更新する次の手順に進んでください。

サービスのホスト名は変わっているので、サービスのアプリケーション ID URI も更新します。たとえば、Azure Web サイトとして todolistservice.azurewebsites.net を使用します。そのため、TodoListService プロジェクトを変更する必要があります。プロジェクトの Global.asax.cs を開き、Azure Web サイトの URL に合わせて対象ユーザーを更新します。たとえば、新しいリソース アプリケーション ID URI になるように、次のように対象ユーザーを更新します。

const string audience = "https://todolistservice.azurewebsites.net";

この変更が完了したら、サービスを展開することができます。

Azure に展開された後は、リソースの ToDo リスト サービスのアプリケーション ID URI は新しくなるため、その変更に合わせてクライアントを更新する必要があります。また、新しい URL を使用するようにクライアントを更新する必要もあります。

MainPage.xaml.cs で、新しいホスティング アドレスを使用するようにリソースの定数を更新します。次に例を示します。

const string resourceAppIDUri = "https://todolistservice.azurewebsites.net";
const string resourceBaseAddress = "https://todolistservice.azurewebsites.net";

クライアントはサービスの正しい URL に接続し、このサービス用のアクセス トークンを AAL に要求するようになります。

最後の手順として、サービスの登録は、Azure Web サイトで実行されている Azure ホスト型の ToDo リスト サービスのインスタンスに追加されます。ToDo リスト クライアントには、Azure ホスト型サービスを呼び出すアクセス許可を付与する必要があります。

この構成を変更するには、Azure の管理ポータルで Web API の情報を更新する必要があります。

  1. Web ブラウザーで、https://manage.windowsazure.com にアクセスします。

  2. Azure の管理ポータルの左側のメニューにある [Active Directory] アイコンをクリックし、目的のディレクトリをクリックします。

  3. ディレクトリの [クイック スタート] ページの上部近くにある [アプリケーション] タブをクリックします。

  4. 登録したアプリケーションのリストから ToDo リスト サービスをクリックします。

  5. アプリケーションの [クイック スタート] ページの上部近くにある [構成] タブをクリックします。

  6. シングル サインオンのセクションで、http://todolistservice.azurewebsites.net アドレスを使用してアプリケーション ID URI と Reply URL を更新し、コマンド バーの [保存] をクリックします。

以上の変更が完了したら、ToDo リスト クライアントを実行し、いくつかの ToDo 項目を Azure Web サイトに保存して試します。ただし、ToDo 項目はサービスのメモリに保存され、サービスはリサイクルされるため、項目は消去されます。

以上でチュートリアルは終了です。このチュートリアルで使用されているテクノロジの詳細については、以下の関連トピックを参照してください。また、このチュートリアルの完成品は、MSDN コード ギャラリーに掲載されている Windows ストア用 AAL のコード サンプルとよく似ています。コード サンプル: Windows ストア アプリケーションから REST サービス - ブラウザー ダイアログを介した AAD による認証

関連項目

この情報は役に立ちましたか。
(残り 1500 文字)
フィードバックをいただき、ありがとうございました

コミュニティの追加

表示:
© 2014 Microsoft