6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Microsoft Graph を使ってみよう : Azure AD v2 エンドポイントでの認証を Web アプリケーション (MVC) と MSAL で行う

Last updated at Posted at 2018-05-28

Web アプリケーションでの認証

ユーザーは以下の図のように、ブラウザ経由で ASP.NET MVC アプリケーションに接続します。Azure AD v2 エンドポイントを利用して自分のサイトをセキュアにできます。

以下、ASP.NET Web アプリへの "Microsoft でサインイン" の追加 より抜粋。
flow

しかし Web アプリケーションが Microsoft Graph に接続してユーザーに関連する情報を取得する場合、ユーザーのアクセストークンが必要となる一方、アクセストークンを要求するのは ASP.NET MVC アプリケーションとなります。そこで MSAL ではそのための手段として、OpenId Connect によるユーザー認証と認可コードの取得、およびサービス側が、認可コードを利用してアクセストークンを取得する仕組みを用意しています。

今回は以下のアプリケーションを作ります。

  • C# MVC Web アプリケーション
  • OpenId と認可コードフローでトークン取得
  • MSAL を利用

OpenId Connect と認可コードフロー

Web アプリケーションにユーザーが OpenId Connect を使用してログインした場合、認可コードが Azure AD より発行されます。Web アプリケーションはこのコードを使って、ユーザーのアクセストークンを取得できます。

アプリケーションの登録

OAuth 2.0 を使うために、まず初めにアプリケーションを登録します。

1. Microsoft アプリ登録ポータル にアクセスして、ログイン。ここでは組織アカウントを使用。

Capture.PNG

2. ユーザー委任シナリオで作成したアプリケーションを利用、または新規に登録。プラットフォームの項目で「プラットフォームの追加」をクリック。Web アプリケーションを選択。

Capture.PNG

3. アプリケーションシークレットの項目で「新しいパスワードを生成」をクリック。パスワードが表示されるのでコピーして保存。
※このパスワードは画面を消すと二度と取得できない。

Capture.PNG

4. Microsoft Graph のアクセス許可の「委任されたアクセス許可」項目で、「追加」をクリック。

Capture.PNG

5. 必要な権限を追加。ここでは Calendars.ReadWrite を選択。

Capture.PNG

6. 最後に「保存」をクリック。

アプリケーションの開発

1. Visual Studio で新しい ASP.NET Web アプリケーションプロジェクトを作成。

image.png

2. MVC を選択。「認証の変更」をクリック。

image.png

3. 「個別のユーザーアカウント」を選択して、「OK」。元の画面に戻るので、再度「OK」をクリックしてプロジェクト作成完了。

image.png

4. NuGet の管理より、「Microsoft.Identity.Client」、「Microsoft.Owin.Security.OpenIdConnect」をインストール。MSAL はプレビューのため、「プレリリースを含める」にチェックを入れる。

image.png

5. 作成したプロジェクトを右クリックしてプロパティを表示。Web のセクションから「プロジェクトの URL」 を確認。

image.png

6. Web.Config の configuration/appSettings 内に必要な情報を追加。

web.config/configuration/appSettings
<add key="ClientId" value="登録したアプリケーションの ID" />
<add key="ClientSecret" value="作成したパスワード" />
<add key="GraphScopes" value="User.Read" />
<add key="RedirectUri" value="プロジェクトの URL" />

7. Microsoft アプリ登録ポータル に戻って、Web プラットフォームの「リダイレクト URL」から「URL の追加」をクリック。プロジェクトの URL を追加後、「保存」。

image.png

8. ユーザーのログインとトークン管理のヘルパークラスを作成。プロジェクトに TokenStorage フォルダを作成し、SessionTokenCache.cs ファイルを追加。

image.png

9. 中身を以下のコードと書き換え。尚コードは GitHub にある aspnet-connect-rest-sample のものを拝借。これまでの記事でも紹介した通り、MSAL が利用する UserTokenCache に利用される。

SessionTokenCache.cs
/* 
*  Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. 
*  See LICENSE in the source repository root for complete license information. 
*/

using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;

namespace GraphWebAppDemo.TokenStorage
{

    // Store the user's token information.
    public class SessionTokenCache
    {
        private static ReaderWriterLockSlim SessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        string UserId = string.Empty;
        string CacheId = string.Empty;
        HttpContextBase httpContext = null;

        TokenCache cache = new TokenCache();

        public SessionTokenCache(string userId, HttpContextBase httpcontext)
        {
            // not object, we want the SUB
            UserId = userId;
            CacheId = UserId + "_TokenCache";
            httpContext = httpcontext;
            Load();
        }

        public TokenCache GetMsalCacheInstance()
        {
            cache.SetBeforeAccess(BeforeAccessNotification);
            cache.SetAfterAccess(AfterAccessNotification);
            Load();
            return cache;
        }

        public void SaveUserStateValue(string state)
        {
            SessionLock.EnterWriteLock();
            httpContext.Session[CacheId + "_state"] = state;
            SessionLock.ExitWriteLock();
        }
        public string ReadUserStateValue()
        {
            string state = string.Empty;
            SessionLock.EnterReadLock();
            state = (string)httpContext.Session[CacheId + "_state"];
            SessionLock.ExitReadLock();
            return state;
        }
        public void Load()
        {
            SessionLock.EnterReadLock();
            cache.Deserialize((byte[])httpContext.Session[CacheId]);
            SessionLock.ExitReadLock();
        }

        public void Persist()
        {
            SessionLock.EnterWriteLock();

            // Optimistically set HasStateChanged to false. We need to do it early to avoid losing changes made by a concurrent thread.
            cache.HasStateChanged = false;

            // Reflect changes in the persistent store
            httpContext.Session[CacheId] = cache.Serialize();
            SessionLock.ExitWriteLock();
        }

        // Triggered right before MSAL needs to access the cache.
        // Reload the cache from the persistent store in case it changed since the last access.
        void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            Load();
        }

        // Triggered right after MSAL accessed the cache.
        void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if the access operation resulted in a cache update
            if (cache.HasStateChanged)
            {
                Persist();
            }
        }

    }
}

10. App_Start フォルダ内 Startup.Auth.cs を以下のコードに書き換えて、OpenId Connect を利用するように変更。

Startup.Auth.cs
using System.Web;
using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using System.Configuration;
using System.Threading.Tasks;
using GraphWebAppDemo.TokenStorage;
using System.IdentityModel.Tokens;
using System.IdentityModel.Claims;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Tokens;

namespace GraphWebAppDemo
{
    public partial class Startup
    {
        private static string clientId = ConfigurationManager.AppSettings["ClientId"];
        private static string clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
        private static string redirectUri = ConfigurationManager.AppSettings["RedirectUri"];
        private static string graphScopes = ConfigurationManager.AppSettings["GraphScopes"];

        public void ConfigureAuth(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = clientId,
                    Authority = "https://login.microsoftonline.com/common/v2.0",
                    PostLogoutRedirectUri = redirectUri,
                    RedirectUri = redirectUri,
                    Scope = "openid email profile offline_access " + graphScopes,
                    TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = false,
                        // 本番運用のサーバーでは Issuer を検証し、正しいアプリケーションから戻ってきたものか確認。 
                        //     IssuerValidator = (issuer, token, tvp) =>
                        //     {
                        //         if (MyCustomTenantValidation(issuer)) 
                        //             return issuer;
                        //         else
                        //             throw new SecurityTokenInvalidIssuerException("Invalid issuer");
                        //     },
                    },
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthorizationCodeReceived = async (context) =>
                        {
                            var code = context.Code;
                            string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

                            TokenCache userTokenCache = new SessionTokenCache(signedInUserID,
                                context.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
                            ConfidentialClientApplication cca = new ConfidentialClientApplication(
                                clientId,
                                redirectUri,
                                new ClientCredential(clientSecret),
                                userTokenCache,
                                null);
                            string[] scopes = graphScopes.Split(new char[] { ' ' });

                            AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, scopes);
                        },
                        AuthenticationFailed = (context) =>
                        {
                            context.HandleResponse();
                            context.Response.Redirect("/Error?message=" + context.Exception.Message);
                            return Task.FromResult(0);
                        }
                    }
                });
        }
    }
}

11. サインアウト時に OpenId Connect のキャッシュを消せるよう、AccountController.cs の LogOff メソッドを以下のコードで書き換え。using の追加を必要に応じて追加。

AccountController.cs
//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
    if (Request.IsAuthenticated)
    {
        HttpContext.GetOwinContext().Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
    }

    return RedirectToAction("Index", "Home");
}

12. またログインでも OpenId Connect を利用するよう、Login メソッドを以下に書き換え

AccountController.cs
//
// GET: /Account/Login
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
    if (!Request.IsAuthenticated)
    {
        // Signal OWIN to send an authorization request to Azure.
        HttpContext.GetOwinContext().Authentication.Challenge(
            new AuthenticationProperties { RedirectUri = "/" },
            OpenIdConnectAuthenticationDefaults.AuthenticationType);
    }
    ViewBag.ReturnUrl = returnUrl;
    return View();
}

13. AntiForgery の対応として、Global.asax.cs の Application_Start メソッドを以下のコードと差し替え。

Global.asax.cs
protected void Application_Start()
{
    AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

14. Graph を使うサンプルとして HomeController.cs の About メソッドを以下に書き換え。

HomeController.cs
using GraphWebAppDemo.TokenStorage;
using Newtonsoft.Json.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Configuration;
using System.Threading.Tasks;
using System.Net.Http.Headers;

[Authorize]
public async Task<ActionResult> About()
{
    string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
    TokenCache userTokenCache = new SessionTokenCache(signedInUserID, HttpContext).GetMsalCacheInstance();
    ConfidentialClientApplication cca = new ConfidentialClientApplication(
            clientId,
            redirectUri,
            new ClientCredential(clientSecret),
            userTokenCache, null);

    // Get an access token.
    var authResult = await cca.AcquireTokenSilentAsync(graphScopes.Split(new char[] { ' ' }), cca.Users.FirstOrDefault());

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

        var httpResponse = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
        var me = JToken.Parse(await httpResponse.Content.ReadAsStringAsync());
        ViewBag.Message = me.Value<string>("displayName");
    }

    return View();
}

アプリケーションのテスト

1. F5 キーを押下してデバッグ実行。画面が起動したら「ログイン」をクリック。

image.png

2. サインインページが出るので、サインイン。

image.png

3. 「詳細」をクリック。名前が取得できることを確認。

image.png

コードの詳細確認

OpenId Connect と MSAL の連携
MVC アプリケーションには OpenId Connect を利用してサインインしていますが、Scope として Microsoft Graph の権限も指定しています。これにより必要なアクセストークンを取得するための認可コードが Azure AD より返されます。

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions
    {
        ClientId = clientId,
        Authority = "https://login.microsoftonline.com/common/v2.0",
        PostLogoutRedirectUri = redirectUri,
        RedirectUri = redirectUri,
        Scope = "openid email profile offline_access " + graphScopes,
        TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            // 本番運用のサーバーでは Issuer を検証し、正しいアプリケーションから戻ってきたものか確認。 
            //     IssuerValidator = (issuer, token, tvp) =>
            //     {
            //         if (MyCustomTenantValidation(issuer)) 
            //             return issuer;
            //         else
            //             throw new SecurityTokenInvalidIssuerException("Invalid issuer");
            //     },
        },
...

認可トークン取得成功時のコールバック関数で、MSAL の ConfidentialClientApplication を作成し、AcquireTokenByAuthorizationCodeAsync メソッドを実行して有効なアクセストークンが取得できるか検証するとともに、UserToken キャッシュにユーザー情報をキャッシュしています。尚、一度でも認可トークンが発行された場合、期限が切れるか、サインアウトするまで、コールバックは発生しません。

Notifications = new OpenIdConnectAuthenticationNotifications
{
    AuthorizationCodeReceived = async (context) =>
    {
        var code = context.Code;
        string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

        TokenCache userTokenCache = new SessionTokenCache(signedInUserID,
            context.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
        ConfidentialClientApplication cca = new ConfidentialClientApplication(
            clientId,
            redirectUri,
            new ClientCredential(clientSecret),
            userTokenCache,
            null);
        string[] scopes = graphScopes.Split(new char[] { ' ' });

        AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, scopes);
    },
...

OpenId Connect へのログインとログアウト
Owin の Authentication.Challenge を呼び出してログインを実行しています。

HttpContext.GetOwinContext().Authentication.Challenge(
    new AuthenticationProperties { RedirectUri = "/" },
    OpenIdConnectAuthenticationDefaults.AuthenticationType)

またログアウト時も Owin の Authentication.SignOut を呼び出して OpenId Connect およびクッキー認証ともにサインアウトしています。

HttpContext.GetOwinContext().Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);           

Microsoft Graph の呼び出し
既にユーザートークンのキャッシュが終わっているため、ConfidentialClientApplication の AcquireTokenSilentAsync メソッドを、ユーザーを指定して実行しています。上記の OpenId Connect と認可コードの処理が失敗している場合は、必然的にこちらのコードも失敗します。

string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new SessionTokenCache(signedInUserID, HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(
        clientId,
        redirectUri,
        new ClientCredential(clientSecret),
        userTokenCache, null);

// Get an access token.
var authResult = await cca.AcquireTokenSilentAsync(graphScopes.Split(new char[] { ' ' }), cca.Users.FirstOrDefault());

using (HttpClient client = new HttpClient())
{
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

    var httpResponse = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
    var me = JToken.Parse(await httpResponse.Content.ReadAsStringAsync());
    ViewBag.Message = me.Value<string>("displayName");
}

ログインユーザーの表示名を取得する

現在、サインインしたにも関わらず、アプリケーションではユーザーの名前が取得できていません。これはユーザーのクレーム情報に名前が含まれないからです。
image.png

Azure AD v2 の OpenId Connect 制限

プロトコルに関する制限事項 にある通り、OpenId Connect でスコープの Profile を指定しても、ユーザー情報が返りません。そこで OpenId Connect にログイン後、さらに Microsoft Graph にアクセスし、名前を補完してみます。

1. Startup.Auth.cs のAuthorizationCodeReceived コールバックを以下のコードと差し替え。

Startup.Auth.cs
AuthorizationCodeReceived = async (context) =>
{
    var code = context.Code;
    string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

    TokenCache userTokenCache = new SessionTokenCache(signedInUserID,
        context.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
    ConfidentialClientApplication cca = new ConfidentialClientApplication(
        clientId,
        redirectUri,
        new ClientCredential(clientSecret),
        userTokenCache,
        null);
    string[] scopes = graphScopes.Split(new char[] { ' ' });

    AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, scopes);

    using (HttpClient client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

        var httpResponse = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
        var me = JToken.Parse(await httpResponse.Content.ReadAsStringAsync());
        System.Security.Claims.ClaimsIdentity claimsId = context.AuthenticationTicket.Identity;
        claimsId.AddClaim(new System.Security.Claims.Claim(
            System.Security.Claims.ClaimTypes.Name, me.Value<string>("displayName"), System.Security.Claims.ClaimValueTypes.String));
    }
},

2. F5 キーを押下してプログラムを実行。サインイン状態の場合は一旦ログオフして、再度サインイン。名前が出ることを確認。

image.png

コードの詳細確認

ではコードの中身を見ていきましょう。

Microsoft Graph からユーザー情報の取得
認可コードから AuthenticationResult を取得した後、そのまま /me にアクセスして名前を取得。

using (HttpClient client = new HttpClient())
{
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
    var httpResponse = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
    var me = JToken.Parse(await httpResponse.Content.ReadAsStringAsync());
...

クレームを追加
現在のクレーム ID を取得して、名前クレームを追加。

System.Security.Claims.ClaimsIdentity claimsId = context.AuthenticationTicket.Identity;
claimsId.AddClaim(new System.Security.Claims.Claim(
        System.Security.Claims.ClaimTypes.Name, me.Value<string>("displayName"), 
        System.Security.Claims.ClaimValueTypes.String));

増分同意を追加する

Azure AD v2 の機能として増分同意機能がありますが、現在は OpenId Connect でログインした際に取得した認可トークンを使っているため、増分同意に対応していません。

今回は「問い合わせ」メニューを変更して、連絡先一覧を取得できるようにし、増分同意を実装します。

1. まずアプリケーションに権限を追加。Microsoft アプリ登録ポータル にログインして、「委任されたアクセス許可」に Contacts.Read 権限を追加して保存。

image.png

2. 次に Models フォルダーに Contact.cs ファイルを追加し、以下のコードを差し替え。

Contact.cs
using Newtonsoft.Json;
using System.Collections.Generic;

namespace GraphWebAppDemo.Models
{
    public class Contact
    {
        [JsonProperty("id")]
        public string ContactId { get; set; }
        [JsonProperty("displayName")]
        public string DisplayName { get; set; }
        [JsonProperty("jobTitle")]
        public string JobTitle { get; set; }
        [JsonProperty("emailAddresses")]
        public List<EmailAddress> EmailAddresses { get; set; }

        public class EmailAddress
        {
            [JsonProperty("name")]
            public string Name { get; set; }
            [JsonProperty("address")]
            public string Address { get; set; }
        }
    }
}

3. Shared フォルダにある _Layout.cshtml の 「問い合わせ」を「連絡先」に変更。

_Layout.cshtml
<li>@Html.ActionLink("ホーム", "Index", "Home")</li>
<li>@Html.ActionLink("詳細", "About", "Home")</li>
<li>@Html.ActionLink("連絡先", "Contact", "Home")</li>

4. HomeController.cs の Contact メソッドを以下のコードに差し替え。

HomeController.cs
public async Task<ActionResult> Contact()
{
    string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
    TokenCache userTokenCache = new SessionTokenCache(signedInUserID, HttpContext).GetMsalCacheInstance();
    ConfidentialClientApplication cca = new ConfidentialClientApplication(
            clientId,
            redirectUri,
            new ClientCredential(clientSecret),
            userTokenCache, null);

    var scopes = new string[] { "Contacts.Read" };

    try
    {
        var authResult = await cca.AcquireTokenSilentAsync(scopes, cca.Users.FirstOrDefault());
        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

            var httpResponse = await client.GetAsync("https://graph.microsoft.com/v1.0/me/contacts");
            var resultJson = JToken.Parse(await httpResponse.Content.ReadAsStringAsync());
            var contacts = JsonConvert.DeserializeObject<List<Contact>>(resultJson["value"].ToString());

            return View(contacts);
        }
    }
    catch (MsalUiRequiredException)
    { 
        try
        {
            string authReqUrl = (await cca.GetAuthorizationRequestUrlAsync(scopes, null, null)).ToString();
            ViewBag.AuthorizationRequest = authReqUrl;
        }
        catch (Exception ee)
        {
            Response.Write(ee.Message);
        }
    }
            
    return View();
}

5. 最後に画面の対応。Views/Home フォルダの Contact.cshtml を以下のコードに差し替え。

Contact.cshtml
@model List<GraphWebAppDemo.Models.Contact>
@{
    ViewBag.Title = "連絡先一覧";
}

<h2>@ViewBag.Title</h2>

@if (ViewBag.AuthorizationRequest != null)
{
    <text>
        追加の同意が必要です。<a href="@ViewBag.AuthorizationRequest">こちら</a>をクリックしてください。
    </text>
}
else
{
    foreach (var contact in Model)
    {
        <text>名前: @contact.DisplayName</text><br />
        <text>タイトル: @contact.JobTitle</text><br />
        if (contact.EmailAddresses.Count > 0)
        {
            <text>メール: @contact.EmailAddresses.First().Address</text><br />
        }
    }
}

6. F5 キーを押下してデバッグ開始。ログインしている場合は一旦ログオフして、再度ログイン。その後「連絡先」メニューをクリック。追加同意を求められる。

image.png

7. リンクをクリックするとサインインが求められるので、サインイン。ここで追加の同意が求められる。

image.png

8. 同意後、再度連絡先をクリック。一覧が取得できていることを確認。

image.png

##コードの詳細確認

追加した内容を確認してみましょう。

アクセストークン取得失敗の処理
スコープが変わり、まだ同意が取れていない場合は、AcquireTokenSilentAsync が失敗します。そこで増分同意のアドレスを GetAuthorizationRequestUrlAsync メソッドで取得して画面に返します。

try
{
    var authResult = await cca.AcquireTokenSilentAsync(scopes, cca.Users.FirstOrDefault());
...
}
catch (MsalUiRequiredException)
{ 
    try
    {
        string authReqUrl = (await cca.GetAuthorizationRequestUrlAsync(scopes, null, null)).ToString();
        ViewBag.AuthorizationRequest = authReqUrl;
    }
...
}

Microsoft Graph の呼び出し
前回同様 HttpClient を使っています。/me/contacts はコレクションを返すため、JSON のパースも value プロパティを配列としてパースしています。

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

var httpResponse = await client.GetAsync("https://graph.microsoft.com/v1.0/me/contacts");
var resultJson = JToken.Parse(await httpResponse.Content.ReadAsStringAsync());
var contacts = JsonConvert.DeserializeObject<List<Contact>>(resultJson["value"].ToString());

return View(contacts);

Contact クラス
MVC でモデルを画面にバインドするには、クラスとして定義されたオブジェクトが使いやすいため、今回は Contact クラスを作成しました。

考慮事項

ユーザートークンキャッシュの場所
クライアントアプリケーションと異なり、Web アプリケーションは複数インスタンスで稼働する可能性が高い他、状況によってはサーバーが再起動されます。今回の開発でもアプリケーションを再起動するたびに、サインアウト/サインインが必要になったため、複数のインスタンスからアクセスでき、かつ永続化されるストレージにキャッシュを保存するようにしてください。

まとめ

今回は MVC を例に Web アプリケーションにおける Microsoft Graph の利用を見ていきましたが、OpenId Connect のスコープに Microsoft Graph の権限を指定し、取得した認可コードを使ってアクセストークンを取得するフローは、他の Web アプリケーションでも同様です。GitHub にある多くのサンプルも同じパターンで実装しています。

目次へ戻る

参照

Azure Active Directory v2.0 と OpenID Connect プロトコル
v2.0 プロトコル: OAuth 2.0 承認コード フロー
ASP.NET Web アプリへの "Microsoft でサインイン" の追加
aspnet-connect-rest-sample
active-directory-dotnet-webapp-openidconnect-v2

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?