英語で読む

次の方法で共有

Microsoft Microsoft Teamsを使用してアプリをビルドGraph

Azure AD 認証を追加する

この演習では、前の演習からアプリケーションを拡張して、Azure サーバーを使用したシングル サインオン認証をAD。 これは、Microsoft Graph API を呼び出すのに必要な OAuth アクセス トークンを取得するために必要です。 この手順では 、Microsoft.Identity.Web ライブラリを構成 します。

重要

アプリケーション ID とシークレットをソースに格納しないようにするには 、.NET Secret Manager を使用してこれらの値を格納します。 シークレット マネージャーは開発のみを目的としますが、実稼働アプリでは、シークレットを格納するために信頼できるシークレット マネージャーを使用する必要があります。

  1. ./appsettings.jsを開き、その内容を次に置き換えてください。

    {
      "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "TenantId": "common"
      },
      "Graph": {
        "Scopes": "https://graph.microsoft.com/.default"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*"
    }
    
  2. GraphTutorial.csproj があるディレクトリで CLI を開き、次のコマンドを実行し、Azure portal のアプリケーション ID に置き換え、アプリケーション シークレットを使用 YOUR_APP_ID YOUR_APP_SECRET します。

    dotnet user-secrets init
    dotnet user-secrets set "AzureAd:ClientId" "YOUR_APP_ID"
    dotnet user-secrets set "AzureAd:ClientSecret" "YOUR_APP_SECRET"
    

サインインの実装

まず、アプリの JavaScript コードにシングル サインオンを実装します。 Microsoft Teams JavaScript SDKを使用して、Teams クライアントで実行されている JavaScript コードが後で実装する Web API への AJAX 呼び出しを行うアクセス トークンを取得します。

  1. ./Pages/Index.cshtml を 開き、タグ内に次のコードを追加 <script> します。

    (function () {
      if (microsoftTeams) {
        microsoftTeams.initialize();
    
        microsoftTeams.authentication.getAuthToken({
          successCallback: (token) => {
            // TEMPORARY: Display the access token for debugging
            $('#tab-container').empty();
    
            $('<code/>', {
              text: token,
              style: 'word-break: break-all;'
            }).appendTo('#tab-container');
          },
          failureCallback: (error) => {
            renderError(error);
          }
        });
      }
    })();
    
    function renderError(error) {
      $('#tab-container').empty();
    
      $('<h1/>', {
        text: 'Error'
      }).appendTo('#tab-container');
    
      $('<code/>', {
        text: JSON.stringify(error, Object.getOwnPropertyNames(error)),
        style: 'word-break: break-all;'
      }).appendTo('#tab-container');
    }
    

    この呼び出しは、ユーザーがユーザーにサインインしているユーザーとしてサイレント microsoftTeams.authentication.getAuthToken 認証Teams。 通常、ユーザーが同意する必要がない限り、UI プロンプトは関与しません。 次に、コードはタブにトークンを表示します。

  2. CLI で次のコマンドを実行して、変更を保存してアプリケーションを起動します。

    dotnet run
    

    重要

    ngrok を再起動し、ngrok URL が変更された場合は、テストする前に、必ず次の場所で ngrok 値 更新してください。

    • アプリ登録のリダイレクト URI
    • アプリ登録のアプリケーション ID URI
    • contentUrl in manifest.json
    • validDomains in manifest.json
    • resource in manifest.json
  3. ZIP ファイルを作成し **、manifest.js、color.png、**およびoutline.png。 ****

  4. [Microsoft Teams] で、左側のバーで [アプリ] を選択し、カスタム アプリアップロードを選択し、自分または自分のチームアップロードを 選択します

    アプリ内のカスタム アップロードリンクのスクリーンショットMicrosoft Teams

  5. 前に作成した ZIP ファイルを参照し、[開く] を 選択します

  6. アプリケーション情報を確認し、[追加] を 選択します

  7. アプリケーションは、アクセス トークンTeams開き、アクセス トークンを表示します。

トークンをコピーする場合は、トークンをトークンに貼り jwt.ms。 対象ユーザー (クレーム) がアプリケーション ID であり、唯一のスコープ (クレーム) が aud scp access_as_user 作成した API スコープを確認します。 つまり、このトークンは Microsoft サービスへの直接アクセスを許可Graph! 代わりに、すぐに実装する Web API は、Microsoft Graph 呼び出しで動作するトークンを取得するために、代理フローを使用してこのトークンを交換する必要があります。

アプリで認証を ASP.NET Coreする

まず、Microsoft Identity プラットフォーム サービスをアプリケーションに追加します。

  1. ./Startup.cs ファイルを開 き、ファイルの上部に using 次のステートメントを追加します。

    using Microsoft.Identity.Web;
    
  2. 関数の行の直前に次 app.UseAuthorization(); の行を追加 Configure します。

    app.UseAuthentication();
    
  3. 関数の行の直後に次 endpoints.MapRazorPages(); の行を追加 Configure します。

    endpoints.MapControllers();
    
  4. 既存の ConfigureServices 関数を、以下の関数で置き換えます。

    public void ConfigureServices(IServiceCollection services)
    {
        // Use Web API authentication (default JWT bearer token scheme)
        services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
            // Enable token acquisition via on-behalf-of flow
            .EnableTokenAcquisitionToCallDownstreamApi()
            // Specify that the down-stream API is Graph
            .AddMicrosoftGraph(Configuration.GetSection("Graph"))
            // Use in-memory token cache
            // See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization
            .AddInMemoryTokenCaches();
    
        services.AddRazorPages();
        services.AddControllers().AddNewtonsoftJson();
    }
    

    このコードは、ヘッダー内の JWT ベアラー トークンに基づいて Web API の呼び出しを認証できるアプリケーションを構成 Authorization します。 また、そのトークンを代理フロー経由で交換できるトークン取得サービスも追加されます。

Web API コントローラーの作成

  1. Controllers という名前のプロジェクトのルートに新しいディレクトリ を作成します

  2. CalendarController.cs という名前の ./Controllers ディレクトリに新しいファイルを作成し、次のコードを追加します。

    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using Microsoft.Identity.Web;
    using Microsoft.Identity.Web.Resource;
    using Microsoft.Graph;
    using TimeZoneConverter;
    
    namespace GraphTutorial.Controllers
    {
        [ApiController]
        [Route("[controller]")]
        [Authorize]
        public class CalendarController : ControllerBase
        {
            private static readonly string[] apiScopes = new[] { "access_as_user" };
    
            private readonly GraphServiceClient _graphClient;
            private readonly ITokenAcquisition _tokenAcquisition;
            private readonly ILogger<CalendarController> _logger;
    
            public CalendarController(ITokenAcquisition tokenAcquisition, GraphServiceClient graphClient, ILogger<CalendarController> logger)
            {
                _tokenAcquisition = tokenAcquisition;
                _graphClient = graphClient;
                _logger = logger;
            }
    
            [HttpGet]
            public async Task<ActionResult<string>> Get()
            {
                // This verifies that the access_as_user scope is
                // present in the bearer token, throws if not
                HttpContext.VerifyUserHasAnyAcceptedScope(apiScopes);
    
                // To verify that the identity libraries have authenticated
                // based on the token, log the user's name
                _logger.LogInformation($"Authenticated user: {User.GetDisplayName()}");
    
                try
                {
                    // TEMPORARY
                    // Get a Graph token via OBO flow
                    var token = await _tokenAcquisition
                        .GetAccessTokenForUserAsync(new[]{
                            "User.Read",
                            "MailboxSettings.Read",
                            "Calendars.ReadWrite" });
    
                    // Log the token
                    _logger.LogInformation($"Access token for Graph: {token}");
                    return Ok("{ \"status\": \"OK\" }");
                }
                catch (MicrosoftIdentityWebChallengeUserException ex)
                {
                    _logger.LogError(ex, "Consent required");
                    // This exception indicates consent is required.
                    // Return a 403 with "consent_required" in the body
                    // to signal to the tab it needs to prompt for consent
                    return new ContentResult {
                        StatusCode = (int)HttpStatusCode.Forbidden,
                        ContentType = "text/plain",
                        Content = "consent_required"
                    };
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error occurred");
                    throw;
                }
            }
        }
    }
    

    これにより、Web API ( ) が実装され、このタブから呼び GET /calendar Teamsできます。今のところ、ベアラー トークンをユーザー トークンと交換Graphします。 ユーザーが初めてタブを読み込む場合、アプリが自分の代わりに Microsoft Graphにアクセスできると同意していないので、これは失敗します。

  3. ./Pages/Index.cshtml を 開き、関数を successCallback 次に置き換える。

    successCallback: (token) => {
      // TEMPORARY: Call the Web API
      fetch('/calendar', {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      }).then(response => {
        response.text()
          .then(body => {
            $('#tab-container').empty();
            $('<code/>', {
              text: body
            }).appendTo('#tab-container');
          });
      }).catch(error => {
        console.error(error);
        renderError(error);
      });
    }
    

    これにより、Web API が呼び出され、応答が表示されます。

  4. 変更内容を保存し、アプリを再起動します。 [ページ] のタブをMicrosoft Teams。 ページが表示されます consent_required

  5. CLI でログ出力を確認します。 2 つの点に注意してください。

    • のようなエントリ Authenticated user: MeganB@contoso.com 。 Web API は、API 要求と一緒に送信されたトークンに基づいてユーザーを認証しました。
    • のようなエントリ AADSTS65001: The user or administrator has not consented to use the application with ID... 。 要求された Microsoft アクセス許可スコープに対する同意を求めるメッセージがまだユーザーにGraphされます。

Web API はユーザーにプロンプトを表示できないので、[Teams] タブでプロンプトを実装する必要があります。 これは、ユーザーごとに 1 回だけ実行する必要があります。 ユーザーが同意すると、アプリケーションへのアクセスを明示的に取り消していない限り、ユーザーは同意を再調整する必要が生じしません。

  1. Authenticate.cshtml.cs という名前の ./Pages ディレクトリに新しいファイルを作成し、次のコードを追加します。

    using Microsoft.AspNetCore.Mvc.RazorPages;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Logging;
    
    namespace GraphTutorial.Pages
    {
        public class AuthenticateModel : PageModel
        {
            private readonly ILogger<IndexModel> _logger;
            public string ApplicationId { get; private set; }
            public string State { get; private set; }
            public string Nonce { get; private set; }
    
            public AuthenticateModel(IConfiguration configuration,
              ILogger<IndexModel> logger)
            {
                _logger = logger;
    
                // Read the application ID from the
                // configuration. This is used to build
                // the authorization URL for the consent prompt
                ApplicationId = configuration
                    .GetSection("AzureAd")
                    .GetValue<string>("ClientId");
    
                // Generate a GUID for state and nonce
                State = System.Guid.NewGuid().ToString();
                Nonce = System.Guid.NewGuid().ToString();
            }
        }
    }
    
  2. Authenticate.cshtml という名前の ./Pages ディレクトリに新しいファイルを作成し、次のコードを追加します。

    @page
    <!-- Copyright (c) Microsoft Corporation.
         Licensed under the MIT License. -->
    @model AuthenticateModel
    
    @section Scripts
    {
      <script>
        (function () {
          microsoftTeams.initialize();
    
          // Save the state so it can be verified in
          // AuthComplete.cshtml
          localStorage.setItem('auth-state', '@Model.State');
    
          // Get the context for tenant ID and login hint
          microsoftTeams.getContext((context) => {
            // Set all of the query parameters for an
            // authorization request
            const queryParams = {
              client_id: '@Model.ApplicationId',
              response_type: 'id_token token',
              response_mode: 'fragment',
              scope: 'https://graph.microsoft.com/.default openid',
              redirect_uri: `${window.location.origin}/authcomplete`,
              nonce: '@Model.Nonce',
              state: '@Model.State',
              login_hint: context.loginHint,
            };
    
            // Generate the URL
            const authEndpoint = `https://login.microsoftonline.com/${context.tid}/oauth2/v2.0/authorize?${toQueryString(queryParams)}`;
    
            // Browse to the URL
            window.location.assign(authEndpoint);
          });
        })();
    
        // Helper function to build a query string from an object
        function toQueryString(queryParams) {
          let encodedQueryParams = [];
          for (let key in queryParams) {
            encodedQueryParams.push(key + '=' + encodeURIComponent(queryParams[key]));
          }
          return encodedQueryParams.join('&');
        }
      </script>
    }
    
  3. AuthComplete.cshtml という名前の ./Pages ディレクトリに新しいファイルを作成し、次のコードを追加します。

    @page
    <!-- Copyright (c) Microsoft Corporation.
         Licensed under the MIT License. -->
    
    @section Scripts
    {
      <script>
        (function () {
          microsoftTeams.initialize();
    
          const hashParams = getHashParameters();
          if (hashParams['error']) {
            microsoftTeams.authentication.notifyFailure(hashParams['error']);
          } else if (hashParams['access_token']) {
            // Check the state parameter
            const expectedState = localStorage.getItem('auth-state');
            if (expectedState !== hashParams['state']) {
              microsoftTeams.authentication.notifyFailure('StateDoesNotMatch');
            } else {
              // State parameter matches, report success
              localStorage.removeItem('auth-state');
              microsoftTeams.authentication.notifySuccess('Success');
            }
          } else {
            microsoftTeams.authentication.notifyFailure('NoTokenInResponse');
          }
        })();
    
        // Helper function to generate a hash from
        // a query string
        function getHashParameters() {
          let hashParams = {};
          location.hash.substr(1).split('&').forEach(function(item) {
            let s = item.split('='),
            k = s[0],
            v = s[1] && decodeURIComponent(s[1]);
            hashParams[k] = v;
          });
          return hashParams;
        }
      </script>
    }
    
  4. ./Pages/Index.cshtml を 開き、タグ内に次の関数を追加 <script> します。

    function loadUserCalendar(token, callback) {
      // Call the API
      fetch('/calendar', {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      }).then(response => {
        if (response.ok) {
          // Get the JSON payload
          response.json()
            .then(events => {
              callback(events);
            });
        }
        else if (response.status === 403) {
          response.text()
            .then(body => {
              // If the API sent 'consent_required'
              //  we need to prompt the user
              if (body === 'consent_required') {
                promptForConsent((error) => {
                  if (error) {
                    renderError(error);
                  } else {
                    // Retry API call
                    loadUserCalendar(token, callback);
                  }
                });
              }
            });
        }
      }).catch(error => {
        renderError(error);
      });
    }
    
    function promptForConsent(callback) {
      // Cause Teams to popup a window for consent
      microsoftTeams.authentication.authenticate({
        url: `${window.location.origin}/authenticate`,
        width: 600,
        height: 535,
        successCallback: (result) => {
          callback(null);
        },
        failureCallback: (error) => {
          callback(error);
        }
      });
    }
    
  5. タグ内に次の関数を <script> 追加して、Web API からの正常な結果を表示します。

    function renderCalendar(events) {
      $('#tab-container').empty();
    
      $('<pre/>').append($('<code/>', {
        text: JSON.stringify(events, null, 2),
        style: 'word-break: break-all;'
      })).appendTo('#tab-container');
    }
    
  6. 既存のコードを次 successCallback のコードに置き換えます。

    successCallback: (token) => {
      loadUserCalendar(token, (events) => {
        renderCalendar(events);
      });
    }
    
  7. 変更内容を保存し、アプリを再起動します。 [ページ] のタブをMicrosoft Teams。 Microsoft のアクセス許可スコープへの同意を求めるポップアップ ウィンドウGraph必要があります。 受け入れ後、タブが表示されます { "status": "OK" }

    注意

    タブが表示される "FailedToOpenWindow" 場合は、ブラウザーでポップアップ ブロックを無効にして、ページを再読み込みしてください。

  8. ログ出力を確認します。 エントリが表示 Access token for Graph されます。 そのトークンを解析すると、このトークンに[オン] で構成された Microsoft Graphスコープがappsettings.js されます

トークンの保存と更新

この時点で、アプリケーションはアクセス トークンを持ち、API 呼び出しの Authorization ヘッダーに送信されます。 これは、アプリが Microsoft Graph にユーザーの代わりにアクセスできるようにするトークンです。

ただし、このトークンは一時的なものです。 トークンは発行後 1 時間で期限切れになります。 ここで、更新トークンが役に立ちます。 更新トークンを使用すると、ユーザーが再度サインインしなくても、アプリは新しいアクセス トークンを要求できます。

アプリは Microsoft.Identity.Web ライブラリを使用していますので、トークンストレージまたは更新ロジックを実装する必要は一切ない。

アプリはメモリ内トークン キャッシュを使用します。これは、アプリの再起動時にトークンを保持する必要がないアプリで十分です。 実稼働アプリでは、代わりにMicrosoft.Identity.Web ライブラリの分散キャッシュ オプションを使用できます。

この GetAccessTokenForUserAsync メソッドは、トークンの有効期限と更新を処理します。 最初にキャッシュされたトークンをチェックし、有効期限が切れていない場合は、トークンを返します。 有効期限が切れている場合は、キャッシュされた更新トークンを使用して新しい更新トークンを取得します。

コントローラー が依存関係の挿入 を介して取得する GraphServiceClient は、ユーザーに使用する認証プロバイダーで事前 GetAccessTokenForUserAsync に構成されています。