Microsoft Microsoft Teamsを使用してアプリをビルドGraph
このチュートリアルでは、Microsoft Teams および Microsoft ASP.NET Core API を使用して Graph アプリを作成して、ユーザーの予定表情報を取得する方法について説明します。
ヒント
完了したチュートリアルをダウンロードする場合は、リポジトリをダウンロードまたは複製GitHubできます。 アプリ ID とシークレットを使用してアプリを構成する手順については、デモ フォルダーの README ファイルを参照してください。
前提条件
このチュートリアルを開始する前に、開発マシンに次の手順をインストールする必要があります。
また、カスタム アプリのサイドローディングを有効にした Microsoft 365テナントに MicrosoftのTeamsアカウントを持っている必要があります。 Microsoft の仕事用アカウントまたは学校アカウントがない場合、または組織がカスタム Teams アプリサイドローディングを有効にしていない場合は、Microsoft 365 Developer Programにサインアップして無料の Office 365 開発者サブスクリプションを取得できます。
注意
このチュートリアルは、.NET SDK バージョン 5.0.302 で記述されています。 このガイドの手順は、他のバージョンでも動作しますが、テストされていない場合があります。
フィードバック
このチュートリアルに関するフィードバックは、GitHubしてください。
MVC Web アプリ ASP.NET Core作成する
Microsoft Teamsタブ アプリケーションには、ユーザーを認証し、Microsoft クライアントを呼び出す複数のGraph。 この演習では、シングル サインオンを実行してクライアントで認証トークンを取得するタブを実装し、サーバー上の代理フローを使用してそのトークンを交換して Microsoft Graph へのアクセスを取得します。
その他の方法については、以下を参照してください。
- Microsoft Microsoft Teamsを使用して[ファイル] タブをGraph Toolkit。 このサンプルは完全にクライアント側であり、Microsoft Graph Toolkitを使用して認証を処理し、Microsoft Graph。
- Microsoft Teams認証サンプル。 このサンプルには、さまざまな認証シナリオをカバーする複数の例が含まれています。
プロジェクトを作成する
まず、Web アプリ ASP.NET Core作成します。
プロジェクトを作成するディレクトリでコマンド ライン インターフェイス (CLI) を開きます。 次のコマンドを実行します。
dotnet new webapp -o GraphTutorial
プロジェクトが作成されると、現在のディレクトリを GraphTutorial ディレクトリに変更し、CLI で次のコマンドを実行して動作します。
dotnet run
ブラウザーを開き、を参照します
https://localhost:5001
。 すべてが機能している場合は、既定のページが表示 ASP.NET Coreがあります。
重要
localhost の証明書が信頼されていないという警告を受け取った場合は、.NET Core CLI を使用して開発証明書をインストールして信頼できます。 特定のオペレーティング システムの手順については、「ASP.NET Coreで HTTPS を適用する」を参照してください。
NuGet パッケージを追加する
次に進む前に、後で使用NuGet追加のパッケージをインストールします。
- アクセス トークンの認証と要求を行う Microsoft.Identity.Web。
- Microsoft.Identity.Web で構成された Microsoft Graphを追加する Microsoft.Identity.Web.MicrosoftGraph。
- Microsoft。Graphをクリックして、Microsoft.Identity.Web.MicrosoftGraph によってインストールされたこのパッケージのバージョンを更新します。
- タイム ゾーン識別子を IANA 識別子Windows変換するTimeZoneConverter。
依存関係をインストールするには、CLI で次のコマンドを実行します。
dotnet add package Microsoft.Identity.Web --version 1.15.2 dotnet add package Microsoft.Identity.Web.MicrosoftGraph --version 1.15.2 dotnet add package Microsoft.Graph --version 4.1.0 dotnet add package TimeZoneConverter
アプリを設計する
このセクションでは、アプリケーションの基本的な UI 構造を作成します。
ヒント
任意のテキスト エディターを使用して、このチュートリアルのソース ファイルを編集できます。 ただし、Visual Studio Codeデバッグや Intellisense などの追加機能が提供されます。
./Pages/Shared/_Layout.cshtml を開き、コンテンツ全体を次のコードに置き換え、アプリのグローバル レイアウトを更新します。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - GraphTutorial</title> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body class="ms-Fabric"> <div class="container"> <main role="main"> @RenderBody() </main> </div> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="https://statics.teams.cdn.office.net/sdk/v1.7.0/js/MicrosoftTeams.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
これにより、ブートストラップが UI Fluent置き換え、SDKMicrosoft Teams追加され、レイアウトが簡略化されます。
./wwwroot/js/site.jsを 開き、次のコードを追加します。
(function () { // Support Teams themes microsoftTeams.initialize(); // On load, match the current theme microsoftTeams.getContext((context) => { if(context.theme !== 'default') { // For Dark and High contrast, set text to white document.body.style.color = '#fff'; document.body.style.setProperty('--border-style', 'solid'); } }); // Register event listener for theme change microsoftTeams.registerOnThemeChangeHandler((theme)=> { if(theme !== 'default') { document.body.style.color = '#fff'; document.body.style.setProperty('--border-style', 'solid'); } else { // For default theme, remove inline style document.body.style.color = ''; document.body.style.setProperty('--border-style', 'none'); } }); })();
これにより、暗いテーマとハイ コントラスト テーマの既定のテキストの色を変更する単純なテーマ変更ハンドラーが追加されます。
./wwwroot/css/site.css を 開き、その内容を次に置き換えてください。
:root { --border-style: none; } .tab-title { margin-bottom: .5em; } .event-card { margin: .5em; padding: 1em; border-style: var(--border-style); border-width: 1px; border-color: #fff; } .event-card div { margin-bottom: .25em; } .event-card .ms-Icon { margin-right: 10px; float: left; position: relative; top: 3px; } .event-card .ms-Icon--MapPin { top: 2px; } .form-container { max-width: 720px; } .form-label { display: block; margin-bottom: .25em; } .form-input { width: 100%; margin-bottom: .25em; padding: .5em; box-sizing: border-box; } .form-button { padding: .5em; } .result-panel { display: none; padding: 1em; margin: 1em; } .error-msg { color: red; } .success-msg { color: green; }
./Pages/Index.cshtml を開き、その内容を次のコードに置き換えます。
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <div id="tab-container"> <h1 class="ms-fontSize-24 ms-fontWeight-semibold">Loading...</h1> </div> @section Scripts { <script> </script> }
./Startup.cs を開 き、メソッド
app.UseHttpsRedirection();
内の行を削除Configure
します。 これは、ngrok トンネリングが機能するために必要です。
ngrok を実行する
Microsoft Teamsアプリのローカル ホスティングはサポートされていません。 アプリをホストするサーバーは、HTTPS エンドポイントを使用してクラウドから利用できる必要があります。 ローカルでデバッグする場合は、ngrok を使用して、ローカルでホストされているプロジェクトのパブリック URL を作成できます。
CLI を開き、次のコマンドを実行して ngrok を開始します。
ngrok http 5000
ngrok が開始したら、HTTPS 転送 URL をコピーします。 次のように見える必要があります
https://50153897dd4d.ngrok.io
。 この値は、後の手順で必要になります。
重要
無料バージョンの ngrok を使用している場合は、ngrok を再起動する度に転送 URL が変更されます。 同じ URL を保持するには、このチュートリアルを完了するまで ngrok を実行してお勧めします。 ngrok を再起動する必要がある場合は、URL が使用されているすべての場所で URL を更新し、アプリを再インストールする必要Microsoft Teams。
ポータルでアプリを登録する
この演習では、管理者センターを使用して新AD Azure Azure Active Directory作成します。
ブラウザーを開き、Azure Active Directory 管理センターへ移動します。 個人用アカウント (別名: Microsoft アカウント)、または 職場/学校アカウント を使用してログインします。
左側のナビゲーションで [Azure Active Directory] を選択し、それから [管理] で [アプリの登録] を選択します。
[新規登録] を選択します。 [アプリケーション の登録] ページ で、前のセクションでコピーした ngrok 転送 URL を次のように
YOUR_NGROK_URL
設定します。Teams Graph Tutorial
に [名前] を設定します。- [サポートされているアカウントの種類] を [任意の組織のディレクトリ内のアカウントと個人用の Microsoft アカウント] に設定します。
- [リダイレクト URI] で、最初のドロップダウン リストを
Web
に設定し、それからYOUR_NGROK_URL/authcomplete
に値を設定します。
[登録] を選択します。 [チュートリアル Teams Graph] ページで、アプリケーション (クライアント) ID の値をコピーして保存します。次の手順で必要になります。
[管理] の下の [認証] を選択します。 [暗黙的な 付与] セクションを見 つけて 、Access トークンと ID トークン を有効にします。 [保存] を選択します。
[管理] で [証明書とシークレット] を選択します。 [新しいクライアント シークレット] ボタンを選択します。 [説明] に値を入力して、[有効期限] のオプションのいずれかを選び、[追加] を選択します。
クライアント シークレットの値をコピーしてから、このページから移動します。 次の手順で行います。
重要
このクライアント シークレットは今後表示されないため、今必ずコピーするようにしてください。
[管理 ] で [API アクセス 許可] を選択し、[アクセス許可 の追加] を選択します。
[Microsoft Graph] を選択 し、[委任されたアクセス許可] を選択します。
次のアクセス許可を選択し、[アクセス許可の 追加] を選択します。
- Calendars.ReadWrite - これにより、アプリはユーザーの予定表に対する読み取りおよび書き込みを行います。
- MailboxSettings.Read - これにより、アプリはメールボックス設定からユーザーのタイム ゾーン、日付形式、および時刻形式を取得できます。
シングル Teamsを構成する
このセクションでは、アプリの登録を更新して、アプリのシングルサインオンをサポートTeams。
[API の公開] を選択します。 [アプリケーション ID URI] の 横にある [設定] リンクを選択します。 二重スラッシュと GUID の間に ngrok 転送 URL ドメイン名 (末尾にスラッシュ "/" が付加された) を挿入します。 ID 全体は次のように表示されます
api://50153897dd4d.ngrok.io/ae7d8088-3422-4c8c-a351-6ded0f21d615
。[この API で定義されたスコープ] セクションで 、[スコープの 追加] を選択します。 次のようにフィールドに入力し、[スコープの追加] を選択します。
- スコープ名:
access_as_user
- Who同意できますか:管理者とユーザー
- 管理者の同意表示名:
Access the app as the user
- 管理者の同意の説明:
Allows Teams to call the app's web APIs as the current user.
- ユーザーの同意表示名:
Access the app as you
- ユーザーの同意の説明:
Allows Teams to call the app's web APIs as you.
- 状態: 有効
- スコープ名:
[承認済 みクライアント アプリケーション] セクションで、[ クライアント アプリケーション の追加] を選択します。 次の一覧からクライアント ID を入力し、[承認済みスコープ] でスコープを有効にし、[アプリケーションの追加]を選択します。 リスト内のクライアント ID ごとにこのプロセスを繰り返します。
1fec8e78-bce4-4aaf-ab1b-5451cc387264
(Teams/デスクトップ アプリケーション)5e3ce6c0-2b1f-4285-8d4b-75ee78787346
(Teams Web アプリケーション)
アプリ マニフェストの作成
アプリ マニフェストは、アプリがアプリとアプリを統合する方法Microsoft Teams、アプリのインストールに必要な方法について説明します。 このセクションでは、アプリケーション クライアントで App Studio をMicrosoft Teamsマニフェストを生成します。
App Studio がインストールされていない場合は、Teamsインストールします。
App Studio を起動Microsoft Teamsマニフェスト エディター を選択します。
[新 しいアプリを作成する] を選択します。
[アプリの 詳細] ページ で、必要なフィールドに入力します。
注意
[ブランド化] セクションの既定のアイコンを 使用するか 、独自のアイコンをアップロードできます。
左側のメニューで、[機能] の下の [タブ] を選択します。
[個人用 タブの追加**] で [追加] を選択します**。
前のセクションでコピーした転送 URL は、次のようにフィールド
YOUR_NGROK_URL
に入力します。 完了したら、[保存] を選択します。- 名前:
Create event
- エンティティ ID:
createEventTab
- コンテンツ URL:
YOUR_NGROK_URL/newevent
- 名前:
[個人用 タブの追加**] で [追加] を選択します**。
前のセクションでコピーした転送 URL は、次のようにフィールド
YOUR_NGROK_URL
に入力します。 完了したら、[保存] を選択します。- 名前:
Graph calendar
- エンティティ ID:
calendarTab
- コンテンツ URL:
YOUR_NGROK_URL
- 名前:
左側のメニューで、[完了] で [ドメインとアクセス許可] を****選択します。
アプリ登録 から AAD アプリ ID をアプリケーション ID に設定します。
[シングル サインオン] フィールドを アプリ登録からアプリケーション ID URI に設定します。
左側のメニューで、[完了] で [テストと配布] を****選択します。 [ダウンロード ] を選択します。
Manifest という名前のプロジェクトのルートに新しいディレクトリを 作成します。 ダウンロードした ZIP ファイルの内容をこのディレクトリに展開します。
Azure AD 認証を追加する
この演習では、前の演習からアプリケーションを拡張して、Azure サーバーを使用したシングル サインオン認証をAD。 これは、Microsoft Graph API を呼び出すのに必要な OAuth アクセス トークンを取得するために必要です。 この手順では 、Microsoft.Identity.Web ライブラリを構成 します。
重要
アプリケーション ID とシークレットをソースに格納しないようにするには 、.NET Secret Manager を使用してこれらの値を格納します。 シークレット マネージャーは開発のみを目的としますが、実稼働アプリでは、シークレットを格納するために信頼できるシークレット マネージャーを使用する必要があります。
./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": "*" }
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 呼び出しを行うアクセス トークンを取得します。
./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 プロンプトは関与しません。 次に、コードはタブにトークンを表示します。CLI で次のコマンドを実行して、変更を保存してアプリケーションを起動します。
dotnet run
重要
ngrok を再起動し、ngrok URL が変更された場合は、テストする前に、必ず次の場所で ngrok 値 を 更新してください。
- アプリ登録のリダイレクト URI
- アプリ登録のアプリケーション ID URI
contentUrl
in manifest.jsonvalidDomains
in manifest.jsonresource
in manifest.json
ZIP ファイルを作成し **、manifest.js、color.png、**およびoutline.png。 ****
[Microsoft Teams] で、左側のバーで [アプリ] を選択し、カスタム アプリアップロードを選択し、自分または自分のチームアップロードを 選択します。
前に作成した ZIP ファイルを参照し、[開く] を 選択します。
アプリケーション情報を確認し、[追加] を 選択します。
アプリケーションは、アクセス トークンTeams開き、アクセス トークンを表示します。
トークンをコピーする場合は、トークンをトークンに貼り jwt.ms。 対象ユーザー (クレーム) がアプリケーション ID であり、唯一のスコープ (クレーム) が aud
scp
access_as_user
作成した API スコープを確認します。 つまり、このトークンは Microsoft サービスへの直接アクセスを許可Graph! 代わりに、すぐに実装する Web API は、Microsoft Graph 呼び出しで動作するトークンを取得するために、代理フローを使用してこのトークンを交換する必要があります。
アプリで認証を ASP.NET Coreする
まず、Microsoft Identity プラットフォーム サービスをアプリケーションに追加します。
./Startup.cs ファイルを開 き、ファイルの上部に
using
次のステートメントを追加します。using Microsoft.Identity.Web;
関数の行の直前に次
app.UseAuthorization();
の行を追加Configure
します。app.UseAuthentication();
関数の行の直後に次
endpoints.MapRazorPages();
の行を追加Configure
します。endpoints.MapControllers();
既存の
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 コントローラーの作成
Controllers という名前のプロジェクトのルートに新しいディレクトリ を作成します。
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にアクセスできると同意していないので、これは失敗します。./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 が呼び出され、応答が表示されます。
変更内容を保存し、アプリを再起動します。 [ページ] のタブをMicrosoft Teams。 ページが表示されます
consent_required
。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 回だけ実行する必要があります。 ユーザーが同意すると、アプリケーションへのアクセスを明示的に取り消していない限り、ユーザーは同意を再調整する必要が生じしません。
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(); } } }
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> }
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> }
./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); } }); }
タグ内に次の関数を
<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'); }
既存のコードを次
successCallback
のコードに置き換えます。successCallback: (token) => { loadUserCalendar(token, (events) => { renderCalendar(events); }); }
変更内容を保存し、アプリを再起動します。 [ページ] のタブをMicrosoft Teams。 Microsoft のアクセス許可スコープへの同意を求めるポップアップ ウィンドウGraph必要があります。 受け入れ後、タブが表示されます
{ "status": "OK" }
。注意
タブが表示される
"FailedToOpenWindow"
場合は、ブラウザーでポップアップ ブロックを無効にして、ページを再読み込みしてください。ログ出力を確認します。 エントリが表示
Access token for Graph
されます。 そのトークンを解析すると、このトークンに[オン] で構成された Microsoft Graphスコープがappsettings.js されます。
トークンの保存と更新
この時点で、アプリケーションはアクセス トークンを持ち、API 呼び出しの Authorization
ヘッダーに送信されます。 これは、アプリが Microsoft Graph にユーザーの代わりにアクセスできるようにするトークンです。
ただし、このトークンは一時的なものです。 トークンは発行後 1 時間で期限切れになります。 ここで、更新トークンが役に立ちます。 更新トークンを使用すると、ユーザーが再度サインインしなくても、アプリは新しいアクセス トークンを要求できます。
アプリは Microsoft.Identity.Web ライブラリを使用していますので、トークンストレージまたは更新ロジックを実装する必要は一切ない。
アプリはメモリ内トークン キャッシュを使用します。これは、アプリの再起動時にトークンを保持する必要がないアプリで十分です。 実稼働アプリでは、代わりにMicrosoft.Identity.Web ライブラリの分散キャッシュ オプションを使用できます。
この GetAccessTokenForUserAsync
メソッドは、トークンの有効期限と更新を処理します。 最初にキャッシュされたトークンをチェックし、有効期限が切れていない場合は、トークンを返します。 有効期限が切れている場合は、キャッシュされた更新トークンを使用して新しい更新トークンを取得します。
コントローラー が依存関係の挿入 を介して取得する GraphServiceClient は、ユーザーに使用する認証プロバイダーで事前 GetAccessTokenForUserAsync
に構成されています。
予定表ビューを取得する
このセクションでは、アプリケーションに Microsoft Graphを組み込む必要があります。 このアプリケーションでは、Microsoft クライアント ライブラリ for .NET Graphを使用して Microsoft クライアント ライブラリを呼び出Graph。
予定表ビューを取得する
予定表ビューは、2 つの時間の間に発生するユーザーの予定表からのイベントのセットです。 これを使用して、現在の週のユーザーのイベントを取得します。
./Controllers/CalendarController.cs を開き 、CalendarController クラスに次の関数を追加します。
private DateTime GetUtcStartOfWeekInTimeZone(DateTime today, string timeZoneId) { // Time zone returned by Graph could be Windows or IANA style // TimeZoneConverter can take either TimeZoneInfo userTimeZone = TZConvert.GetTimeZoneInfo(timeZoneId); // Assumes Sunday as first day of week int diff = System.DayOfWeek.Sunday - today.DayOfWeek; // create date as unspecified kind var unspecifiedStart = DateTime.SpecifyKind(today.AddDays(diff), DateTimeKind.Unspecified); // convert to UTC return TimeZoneInfo.ConvertTimeToUtc(unspecifiedStart, userTimeZone); }
Microsoft の呼び出しから返される例外を処理するには、次のGraphします。
private async Task HandleGraphException(Exception exception) { if (exception is MicrosoftIdentityWebChallengeUserException) { _logger.LogError(exception, "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 HttpContext.Response.ContentType = "text/plain"; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; await HttpContext.Response.WriteAsync("consent_required"); } else if (exception is ServiceException) { var serviceException = exception as ServiceException; _logger.LogError(serviceException, "Graph service error occurred"); HttpContext.Response.ContentType = "text/plain"; HttpContext.Response.StatusCode = (int)serviceException.StatusCode; await HttpContext.Response.WriteAsync(serviceException.Error.ToString()); } else { _logger.LogError(exception, "Error occurred"); HttpContext.Response.ContentType = "text/plain"; HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await HttpContext.Response.WriteAsync(exception.ToString()); } }
既存の
Get
関数を、以下の関数で置き換えます。[HttpGet] public async Task<IEnumerable<Event>> 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 { // Get the user's mailbox settings var me = await _graphClient.Me .Request() .Select(u => new { u.MailboxSettings }) .GetAsync(); // Get the start and end of week in user's time // zone var startOfWeek = GetUtcStartOfWeekInTimeZone( DateTime.Today, me.MailboxSettings.TimeZone); var endOfWeek = startOfWeek.AddDays(7); // Set the start and end of the view var viewOptions = new List<QueryOption> { new QueryOption("startDateTime", startOfWeek.ToString("o")), new QueryOption("endDateTime", endOfWeek.ToString("o")) }; // Get the user's calendar view var results = await _graphClient.Me .CalendarView .Request(viewOptions) // Send user time zone in request so date/time in // response will be in preferred time zone .Header("Prefer", $"outlook.timezone=\"{me.MailboxSettings.TimeZone}\"") // Get max 50 per request .Top(50) // Only return fields app will use .Select(e => new { e.Subject, e.Organizer, e.Start, e.End, e.Location }) // Order results chronologically .OrderBy("start/dateTime") .GetAsync(); return results.CurrentPage; } catch (Exception ex) { await HandleGraphException(ex); return null; } }
変更を確認します。 この新しいバージョンの関数:
- の代
IEnumerable<Event>
わりに返しますstring
。 - Microsoft アカウントを使用してユーザーのメールボックス設定をGraph。
- ユーザーのタイム ゾーンを使用して、現在の週の開始と終了を計算します。
- 予定表ビューを取得する
- 関数を使用してヘッダーを含め、返されるイベントの開始時刻と終了時刻をユーザーのタイム ゾーン
.Header()
Prefer: outlook.timezone
に変換します。 - この関数を
.Top()
使用して、最大で 50 のイベントを要求します。 - この関数
.Select()
を使用して、アプリで使用されるフィールドを要求します。 - 関数を使用
OrderBy()
して、開始時刻で結果を並べ替える。
- 関数を使用してヘッダーを含め、返されるイベントの開始時刻と終了時刻をユーザーのタイム ゾーン
- の代
変更内容を保存し、アプリを再起動します。 [ページ] のタブをMicrosoft Teams。 アプリは、イベントの JSON リストを表示します。
結果の表示
これで、よりユーザーフレンドリーな方法でイベントの一覧を表示できます。
./Pages/Index.cshtml を 開き、タグ内に次の関数を追加
<script>
します。function renderSubject(subject) { if (!subject || subject.length <= 0) { subject = '<No subject>'; } return $('<div/>', { class: 'ms-fontSize-18 ms-fontWeight-bold', text: subject }); } function renderOrganizer(organizer) { return $('<div/>', { class: 'ms-fontSize-14 ms-fontWeight-semilight', text: organizer.emailAddress.name }).append($('<i/>', { class: 'ms-Icon ms-Icon--PartyLeader', style: 'margin-right: 10px;' })); } function renderTimeSpan(start, end) { return $('<div/>', { class: 'ms-fontSize-14 ms-fontWeight-semilight', text: `${formatDateTime(start.dateTime)} - ${formatDateTime(end.dateTime)}` }).append($('<i/>', { class: 'ms-Icon ms-Icon--DateTime2', style: 'margin-right: 10px;' })); } function formatDateTime(dateTime) { const date = new Date(dateTime); // Format like 10/14/2020 4:00 PM let hours = date.getHours(); const minutes = date.getMinutes(); const ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; hours = hours ? hours : 12; const minStr = minutes < 10 ? `0${minutes}` : minutes; return `${date.getMonth()+1}/${date.getDate()}/${date.getFullYear()} ${hours}:${minStr} ${ampm}`; } function renderLocation(location) { if (!location || location.displayName.length <= 0) { return null; } return $('<div/>', { class: 'ms-fontSize-14 ms-fontWeight-semilight', text: location.displayName }).append($('<i/>', { class: 'ms-Icon ms-Icon--MapPin', style: 'margin-right: 10px;' })); }
既存の
renderCalendar
関数を、以下の関数で置き換えます。function renderCalendar(events) { $('#tab-container').empty(); // Add title $('<div/>', { class: 'tab-title ms-fontSize-42', text: 'Week at a glance' }).appendTo('#tab-container'); // Render each event events.map(event => { const eventCard = $('<div/>', { class: 'event-card ms-depth-4', }); eventCard.append(renderSubject(event.subject)); eventCard.append(renderOrganizer(event.organizer)); eventCard.append(renderTimeSpan(event.start, event.end)); const location = renderLocation(event.location); if (location) { eventCard.append(location); } eventCard.appendTo('#tab-container'); }); }
変更内容を保存し、アプリを再起動します。 [ページ] のタブをMicrosoft Teams。 アプリは、ユーザーの予定表にイベントを表示します。
新しいイベントを作成する
このセクションでは、ユーザーの予定表にイベントを作成する機能を追加します。
[新しいイベント] タブを作成する
NewEvent.cshtml という名前の ./Pages ディレクトリに新しいファイルを作成し、次のコードを追加します。
@page <!-- Copyright (c) Microsoft Corporation. Licensed under the MIT License. --> @{ ViewData["Title"] = "New event"; } <div class="form-container"> <form id="newEventForm"> <div class="ms-Grid" dir="ltr"> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm12"> <label class="ms-fontWeight-semibold form-label" for="subject">Subject</label> <input class="form-input" type="text" id="subject" name="subject" /> </div> </div> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm12"> <label class="ms-fontWeight-semibold form-label" for="attendees">Attendees</label> <input class="form-input" type="text" id="attendees" name="attendees" placeholder="Enter email addresses of attendees. Separate multiple with ';'. Leave blank for no attendees." /> </div> </div> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm6"> <label class="ms-fontWeight-semibold form-label" for="start">Start</label> <input class="form-input" type="datetime-local" id="start" name="start" /> </div> <div class="ms-Grid-col ms-sm6"> <label class="ms-fontWeight-semibold form-label" for="end">End</label> <input class="form-input" type="datetime-local" id="end" name="end" /> </div> </div> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm12"> <label class="ms-fontWeight-semibold form-label" for="body">Body</label> <textarea class="form-input" id="body" name="body" rows="4"></textarea> </div> </div> <input class="form-button" type="submit" value="Create"/> </div> </form> <div class="ms-depth-16 result-panel"></div> </div> @section Scripts { <script> (function () { if (microsoftTeams) { microsoftTeams.initialize(); } $('#newEventForm').on('submit', async (e) => { e.preventDefault(); $('.result-panel').empty(); $('.result-panel').hide(); const formData = new FormData(newEventForm); // Basic validation // Require subject, start, and end const subject = formData.get('subject'); const start = formData.get('start'); const end = formData.get('end'); if (subject.length <= 0 || start.length <= 0 || end.length <= 0) { $('<div/>', { class: 'error-msg', text: 'Subject, Start, and End are required.' }).appendTo('.result-panel'); $('.result-panel').show(); return; } // Get the auth token from Teams microsoftTeams.authentication.getAuthToken({ successCallback: (token) => { createEvent(token, formData); }, failureCallback: (error) => { $('<div/>', { class: 'error-msg', text: `Error getting token: ${error}` }).appendTo('.result-panel'); $('.result-panel').show(); } }); }); })(); async function createEvent(token, formData) { // Convert the form to a JSON payload jsonFormData = formDataToJson(); // Post the payload to the web API const response = await fetch('/calendar', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, method: 'POST', body: jsonFormData }); if (response.ok) { $('<div/>', { class: 'success-msg', text: 'Event added to your calendar' }).appendTo('.result-panel'); $('.result-panel').show(); } else { const error = await response.text(); $('<div/>', { class: 'error-msg', text: `Error creating event: ${error}` }).appendTo('.result-panel'); $('.result-panel').show(); } } // Helper method to serialize the form fields // as JSON function formDataToJson() { const array = $('#newEventForm').serializeArray(); const jsonObj = {}; array.forEach((kvp) => { jsonObj[kvp.name] = kvp.value; }); return JSON.stringify(jsonObj); } </script> }
これにより、単純なフォームが実装され、フォーム データを Web API に投稿する JavaScript が追加されます。
Web API の実装
プロジェクトのルートに Models という名前の新しいディレクトリを作成します。
NewEvent.cs という名前の ./Models ディレクトリに新しいファイルを作成し、次のコードを追加します。
namespace GraphTutorial.Models { public class NewEvent { public string Subject { get; set; } public string Attendees { get; set; } public string Start { get; set; } public string End { get; set; } public string Body { get; set; } } }
./Controllers/CalendarController.cs を開き、ファイルの上部に次
using
のステートメントを追加します。using GraphTutorial.Models;
CalendarController クラスに次 の関数を追加 します。
[HttpPost] public async Task<string> Post(NewEvent newEvent) { HttpContext.VerifyUserHasAnyAcceptedScope(apiScopes); try { // Get the user's mailbox settings var me = await _graphClient.Me .Request() .Select(u => new { u.MailboxSettings }) .GetAsync(); // Create a Graph Event var graphEvent = new Event { Subject = newEvent.Subject, Start = new DateTimeTimeZone { DateTime = newEvent.Start, TimeZone = me.MailboxSettings.TimeZone }, End = new DateTimeTimeZone { DateTime = newEvent.End, TimeZone = me.MailboxSettings.TimeZone } }; // If there are attendees, add them if (!string.IsNullOrEmpty(newEvent.Attendees)) { var attendees = new List<Attendee>(); var emailArray = newEvent.Attendees.Split(';'); foreach (var email in emailArray) { attendees.Add(new Attendee { Type = AttendeeType.Required, EmailAddress = new EmailAddress { Address = email } }); } graphEvent.Attendees = attendees; } // If there is a body, add it if (!string.IsNullOrEmpty(newEvent.Body)) { graphEvent.Body = new ItemBody { ContentType = BodyType.Text, Content = newEvent.Body }; } // Create the event await _graphClient.Me .Events .Request() .AddAsync(graphEvent); return "success"; } catch (Exception ex) { await HandleGraphException(ex); return null; } }
これにより、フォームのフィールドを含む Web API への HTTP POST が可能です。
すべての変更を保存し、アプリケーションを再起動します。 アプリを更新して、[Microsoft Teams]タブを選択 します。フォームに入力し、[作成]を選択 して、ユーザーの予定表にイベントを追加します。
おめでとうございます。
Microsoft アプリのチュートリアルMicrosoft Teams完了Graphしました。 Microsoft Graphを呼び出す作業アプリが作成されたので、新しい機能を試して追加できます。 Microsoft Graphの概要を参照して、Microsoft Graphでアクセスできるすべてのデータを確認Graph。
フィードバック
このチュートリアルに関するフィードバックは、GitHubしてください。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。