Microsoft サービスを使用して Azure 関数をGraph
このチュートリアルでは、Microsoft Graph API を使用してユーザーの予定表情報を取得する Azure 関数を構築する方法について説明します。
ヒント
完了したチュートリアルをダウンロードする場合は、リポジトリをダウンロードまたは複製GitHubできます。 アプリ ID とシークレットを使用してアプリを構成する手順については、デモ フォルダーの README ファイルを参照してください。
前提条件
このチュートリアルを開始する前に、開発マシンに次のツールがインストールされている必要があります。
また、同じ組織のグローバル管理者アカウントにアクセスできる Microsoft の仕事用アカウントまたは学校アカウントを持っている必要があります。 Microsoft アカウントをお持ちでない場合は、開発者プログラムにサインアップMicrosoft 365無料のサブスクリプションをOffice 365できます。
注意
このチュートリアルは、上記のツールの次のバージョンで記述されています。 このガイドの手順は、他のバージョンでも動作しますが、テストされていない場合があります。
- .NET Core SDK 5.0.203
- Azure Functions Core Tools 3.0.3442
- Azure CLI 2.23.0
- ngrok 2.3.40
フィードバック
このチュートリアルに関するフィードバックは、リポジトリのGitHubしてください。
Azure Functions プロジェクトの作成
このチュートリアルでは、Microsoft トリガー関数を呼び出す HTTP トリガー関数を実装する単純な Azure 関数をGraph。 これらの関数は、次のシナリオについて説明します。
- フロー認証の代理を使用してユーザーの受信トレイにアクセスするための API を実装 します。
- クライアント資格情報の許可フロー認証を使用して、ユーザーの受信トレイの通知をサブスクライブおよびサブスクライブ解除する API を実装 します。
- Webhook を実装して、Microsoft Graphから変更通知を受け取り、クライアント資格情報の付与フローを使用してデータにアクセスします。
また、Azure 関数に実装されている API を呼び出す単純な JavaScript シングル ページ アプリケーション (SPA) を作成します。
Azure Functions プロジェクトの作成
プロジェクトを作成するディレクトリでコマンド ライン インターフェイス (CLI) を開きます。 次のコマンドを実行します。
func init GraphTutorial --worker-runtime dotnetisolatedCLI のカレント ディレクトリを GraphTutorial ディレクトリに変更し、次のコマンドを実行してプロジェクトに 3 つの関数を作成します。
func new --name GetMyNewestMessage --template "HTTP trigger" func new --name SetSubscription --template "HTTP trigger" func new --name Notify --template "HTTP trigger"local.settings.json を 開き、次のコードをファイルに追加して、テスト アプリケーションの URL である CORS
http://localhost:8080を許可します。"Host": { "CORS": "http://localhost:8080" }次のコマンドを実行して、プロジェクトをローカルで実行します。
func startすべてが機能している場合は、次の出力が表示されます。
Functions: GetMyNewestMessage: [GET,POST] http://localhost:7071/api/GetMyNewestMessage Notify: [GET,POST] http://localhost:7071/api/Notify SetSubscription: [GET,POST] http://localhost:7071/api/SetSubscriptionブラウザーを開き、出力に表示される関数 URL を参照して、関数が正しく動作しているのを確認します。 ブラウザーに次のメッセージが表示されます。
Welcome to Azure Functions!
単一ページ アプリケーションの作成
プロジェクトを作成するディレクトリで CLI を開きます。 TestClient という名前 のディレクトリを作成 して、HTML ファイルと JavaScript ファイルを保持します。
TestClient ディレクトリ にindex.htmlという 名前の新しいファイルを作成し、次のコードを追加します。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <title>Azure Functions Graph Tutorial Test Client</title> <link rel="shortcut icon" href="g-raph.png"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.1/css/all.css" crossorigin="anonymous"> <link href="style.css" rel="stylesheet" type="text/css" /> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <a href="/" class="navbar-brand">Azure Functions Graph Test Client</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul id="authenticated-nav" class="navbar-nav mr-auto"></ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item"> <a class="nav-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank"> <i class="fas fa-external-link-alt mr-1"></i>Docs </a> </li> <li id="account-nav" class="nav-item"></li> </ul> </div> </div> </nav> <main id="main-container" role="main" class="container"> </main> <!-- Bootstrap/jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> <!-- MSAL --> <script src="https://alcdn.msauth.net/browser/2.0.0/js/msal-browser.min.js" integrity="sha384-n3aacu1eFuIAfS3ZY4WGIZiQG/skqpT+cbeqIwLddpmMWcxWZwYdt+F0PgKyw+m9" crossorigin="anonymous"></script> <script src="config.js"></script> <script src="ui.js"></script> <script src="auth.js"></script> <script src="azurefunctions.js"></script> </body> </html>これにより、ナビゲーション バーを含むアプリの基本的なレイアウトが定義されます。 また、次の追加も行います。
- ブートストラップ とそのサポート JavaScript
- FontAwesome
- JavaScript 用 Microsoft 認証ライブラリ (MSAL.js) 2.0
ヒント
ページには、favicon () が含まれています
<link rel="shortcut icon" href="g-raph.png">。 この行を削除するか、この行から g-raph.pngファイル をGitHub。TestClient ディレクトリに style.css という名前 の新しいファイルを作成し、次のコードを追加します。
body { padding-top: 70px; }TestClient ディレクトリ にui.jsという 名前の新しいファイルを作成し、次のコードを追加します。
// Select DOM elements to work with const authenticatedNav = document.getElementById('authenticated-nav'); const accountNav = document.getElementById('account-nav'); const mainContainer = document.getElementById('main-container'); const Views = { error: 1, home: 2, message: 3, subscriptions: 4 }; // Helper function to create an element, set class, and add text function createElement(type, className, text) { const element = document.createElement(type); element.className = className; if (text) { const textNode = document.createTextNode(text); element.appendChild(textNode); } return element; } // Show the navigation items that should only show if // the user is signed in function showAuthenticatedNav(user, view) { authenticatedNav.innerHTML = ''; if (user) { // Add message link const messageNav = createElement('li', 'nav-item'); const messageLink = createElement('button', `btn btn-link nav-link${view === Views.message ? ' active' : '' }`, 'Latest Message'); messageLink.setAttribute('onclick', 'getLatestMessage();'); messageNav.appendChild(messageLink); authenticatedNav.appendChild(messageNav); // Add subscriptions link const subscriptionNav = createElement('li', 'nav-item'); const subscriptionLink = createElement('button', `btn btn-link nav-link${view === Views.message ? ' active' : '' }`, 'Subscriptions'); subscriptionLink.setAttribute('onclick', `updatePage(${Views.subscriptions});`); subscriptionNav.appendChild(subscriptionLink); authenticatedNav.appendChild(subscriptionNav); } } // Show the sign in button or the dropdown to sign-out function showAccountNav(user) { accountNav.innerHTML = ''; if (user) { // Show the "signed-in" nav accountNav.className = 'nav-item dropdown'; const dropdown = createElement('a', 'nav-link dropdown-toggle'); dropdown.setAttribute('data-toggle', 'dropdown'); dropdown.setAttribute('role', 'button'); accountNav.appendChild(dropdown); const userIcon = createElement('i', 'far fa-user-circle fa-lg rounded-circle align-self-center'); userIcon.style.width = '32px'; dropdown.appendChild(userIcon); const menu = createElement('div', 'dropdown-menu dropdown-menu-right'); dropdown.appendChild(menu); const userName = createElement('h5', 'dropdown-item-text mb-0', user); menu.appendChild(userName); const divider = createElement('div', 'dropdown-divider'); menu.appendChild(divider); const signOutButton = createElement('button', 'dropdown-item', 'Sign out'); signOutButton.setAttribute('onclick', 'signOut();'); menu.appendChild(signOutButton); } else { // Show a "sign in" button accountNav.className = 'nav-item'; const signInButton = createElement('button', 'btn btn-link nav-link', 'Sign in'); signInButton.setAttribute('onclick', 'signIn();'); accountNav.appendChild(signInButton); } } // Renders the home view function showWelcomeMessage(user) { // Create jumbotron const jumbotron = createElement('div', 'jumbotron'); const heading = createElement('h1', null, 'Azure Functions Graph Tutorial Test Client'); jumbotron.appendChild(heading); const lead = createElement('p', 'lead', 'This sample app is used to test the Azure Functions in the Azure Functions Graph Tutorial'); jumbotron.appendChild(lead); if (user) { // Welcome the user by name const welcomeMessage = createElement('h4', null, `Welcome ${user}!`); jumbotron.appendChild(welcomeMessage); const callToAction = createElement('p', null, 'Use the navigation bar at the top of the page to get started.'); jumbotron.appendChild(callToAction); } else { // Show a sign in button in the jumbotron const signInButton = createElement('button', 'btn btn-primary btn-large', 'Click here to sign in'); signInButton.setAttribute('onclick', 'signIn();') jumbotron.appendChild(signInButton); } mainContainer.innerHTML = ''; mainContainer.appendChild(jumbotron); } // Renders an email message function showLatestMessage(message) { // Show message const messageCard = createElement('div', 'card'); const cardBody = createElement('div', 'card-body'); messageCard.appendChild(cardBody); const subject = createElement('h1', 'card-title', `${message.subject || '(No subject)'}`); cardBody.appendChild(subject); const fromLine = createElement('div', 'd-flex'); cardBody.appendChild(fromLine); const fromLabel = createElement('div', 'mr-3'); fromLabel.appendChild(createElement('strong', '', 'From:')); fromLine.appendChild(fromLabel); fromLine.appendChild(createElement('div', '', message.from.emailAddress.name)); const receivedLine = createElement('div', 'd-flex'); cardBody.appendChild(receivedLine); const receivedLabel = createElement('div', 'mr-3'); receivedLabel.appendChild(createElement('strong', '', 'Received:')); receivedLine.appendChild(receivedLabel); receivedLine.appendChild(createElement('div', '', message.receivedDateTime)); mainContainer.innerHTML = ''; mainContainer.appendChild(messageCard); } // Renders current subscriptions from the session, and allows the user // to add new subscriptions function showSubscriptions() { const subscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')); // Show new subscription form const form = createElement('form', 'form-inline mb-3'); const userInput = createElement('input', 'form-control mb-2 mr-2 flex-grow-1'); userInput.setAttribute('id', 'subscribe-user'); userInput.setAttribute('type', 'text'); userInput.setAttribute('placeholder', 'User to subscribe to (user ID or UPN)'); form.appendChild(userInput); const subscribeButton = createElement('button', 'btn btn-primary mb-2', 'Subscribe'); subscribeButton.setAttribute('type', 'button'); subscribeButton.setAttribute('onclick', 'createSubscription();'); form.appendChild(subscribeButton); const card = createElement('div', 'card'); const cardBody = createElement('div', 'card-body'); card.appendChild(cardBody); cardBody.appendChild(createElement('h2', 'card-title mb-4', 'Existing subscriptions')); const subscriptionTable = createElement('table', 'table'); cardBody.appendChild(subscriptionTable); const thead = createElement('thead', ''); subscriptionTable.appendChild(thead); const theadRow = createElement('tr', ''); thead.appendChild(theadRow); theadRow.appendChild(createElement('th', '')); theadRow.appendChild(createElement('th', '', 'User')); theadRow.appendChild(createElement('th', '', 'Subscription ID')) if (subscriptions) { // List subscriptions for (const subscription of subscriptions) { const row = createElement('tr', ''); subscriptionTable.appendChild(row); const deleteButtonCell = createElement('td', ''); row.appendChild(deleteButtonCell); const deleteButton = createElement('button', 'btn btn-sm btn-primary', 'Delete'); deleteButton.setAttribute('onclick', `deleteSubscription("${subscription.subscriptionId}");`); deleteButtonCell.appendChild(deleteButton); row.appendChild(createElement('td', '', subscription.userId)); row.appendChild(createElement('td', '', subscription.subscriptionId)); } } mainContainer.innerHTML = ''; mainContainer.appendChild(form); mainContainer.appendChild(card); } // Renders an error function showError(error) { const alert = createElement('div', 'alert alert-danger'); const message = createElement('p', 'mb-3', error.message); alert.appendChild(message); if (error.debug) { const pre = createElement('pre', 'alert-pre border bg-light p-2'); alert.appendChild(pre); const code = createElement('code', 'text-break text-wrap', JSON.stringify(error.debug, null, 2)); pre.appendChild(code); } mainContainer.innerHTML = ''; mainContainer.appendChild(alert); } // Re-renders the page with the selected view function updatePage(view, data) { if (!view) { view = Views.home; } // Get the user name from the session const user = sessionStorage.getItem('msal-userName'); if (!user && view !== Views.error) { view = Views.home; } showAccountNav(user); showAuthenticatedNav(user, view); switch (view) { case Views.error: showError(data); break; case Views.home: showWelcomeMessage(user); break; case Views.message: showLatestMessage(data); break; case Views.subscriptions: showSubscriptions(); break; } } updatePage(Views.home);このコードでは、JavaScript を使用して、選択したビューに基づいて現在のページをレンダリングします。
単一ページ アプリケーションをテストする
注意
このセクションでは、開発マシンで簡単なテスト HTTP サーバーを実行するために dotnet-serve を使用する手順について説明します。 この特定のツールを使用する必要はありません。 TestClient ディレクトリを提供する任意のテスト サーバー を使用 できます。
CLI で次のコマンドを実行して、 dotnet-serve をインストールします。
dotnet tool install --global dotnet-serveCLI のカレント ディレクトリを TestClient ディレクトリに変更し、次のコマンドを実行して HTTP サーバーを起動します。
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate" -p 8080ブラウザーを開き、
http://localhost:8080に移動します。 ページはレンダリングする必要がありますが、現在機能しているボタンはどれも表示されません。
NuGet パッケージを追加する
次に進む前に、後で使用NuGet追加のパッケージをインストールします。
- Azure Functions プロジェクトで依存関係の挿入を有効にする Microsoft.Azure.Functions.Extensions 。
- Microsoft.Extensions.Configuration.UserSecrets を使用して、.NET 開発シークレット ストアからアプリケーション構成 を読み取る。
- Microsoft.Graph: Microsoft Graph を呼び出すためのものです。
- トークンの認証と管理を行う Microsoft.Identity.Client 。
- トークン検証用の OpenID 構成を取得するための Microsoft.IdentityModel.Protocols.OpenIdConnect 。
- Web API に送信されるトークンを検証するための System.IdentityModel.Tokens.Jwt 。
CLI のカレント ディレクトリを GraphTutorial ディレクトリに変更し、次のコマンドを実行します。
dotnet add package Microsoft.Azure.Functions.Extensions --version 1.1.0 dotnet add package Microsoft.Extensions.Configuration.UserSecrets --version 5.0.0 dotnet add package Microsoft.Graph --version 4.0.0 dotnet add package Microsoft.Identity.Client --version 4.35.1 dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect --version 6.12.0 dotnet add package System.IdentityModel.Tokens.Jwt --version 6.12.0
ポータルにアプリを登録する
この演習では、次の管理センターを使用Azure AD 3 つの新しいAzure Active Directory作成します。
- ユーザーにサインインし、アプリケーションが Azure 関数を呼び出すトークンを取得できるように、シングル ページ アプリケーションのアプリ登録。
- Azure 関数のアプリ登録で、代理フローを使用して、SPA から送信されたトークンを Microsoft Graph を呼び出すトークンと交換できます。
- Azure Function Webhook のアプリ登録で、クライアント資格情報フローを使用してユーザーなしで Microsoft Graph呼び出します。
注意
この例では、代理フローとクライアント資格情報フローの両方を実装するために、3 つのアプリ登録が必要です。 Azure 関数でこれらのフローの 1 つしか使用されていない場合は、そのフローに対応するアプリ登録のみを作成する必要があります。
ブラウザーを開き、管理者センターに移動Azure Active Directoryテナント組織の管理者を使用Microsoft 365ログインします。
左側のナビゲーションで [Azure Active Directory] を選択し、それから [管理] で [アプリの登録] を選択します。

単一ページ アプリケーションのアプリを登録する
[新規登録] を選択します。 [アプリケーションを登録] ページで、次のように値を設定します。
Graph Azure Function Test Appに [名前] を設定します。- [サポート されているアカウントの種類] を [ この組織ディレクトリのアカウントのみ] に設定します。
- [ リダイレクト URI] で、ドロップダウンを [単一ページ アプリケーション (SPA) に変更し、 値をに設定します
http://localhost:8080。
![[アプリケーションを登録する] ページのスクリーンショット](azure-functions/tutorial/images/register-command-line-app.png)
[登録] を選択します。 [Azure Graphテスト アプリ] ページで、アプリケーション (クライアント) ID とディレクトリ (テナント) ID の値をコピーして保存します。後の手順で必要になります。

Azure 関数のアプリを登録する
[アプリの 登録] に戻り、[ 新しい登録] を選択します。 [アプリケーションを登録] ページで、次のように値を設定します。
Graph Azure Functionに [名前] を設定します。- [サポート されているアカウントの種類] を [ この組織ディレクトリのアカウントのみ] に設定します。
- リダイレクト URI は空白 のままにします。
[登録] を選択します。 [Azure Graph] ページ で、アプリケーション (クライアント) ID の値をコピーして保存します。次の手順で必要になります。
[管理] で [証明書とシークレット] を選択します。 [新しいクライアント シークレット] ボタンを選択します。 [説明] に値を入力して、[有効期限] のオプションのいずれかを選び、[追加] を選択します。
![[クライアントシークレットの追加] ダイアログのスクリーンショット](azure-functions/tutorial/images/aad-new-client-secret.png)
このページを離れる前に、クライアント シークレットの値をコピーします。 次の手順で行います。
重要
このクライアント シークレットは今後表示されないため、この段階で必ずコピーするようにしてください。

[管理 ] で [API のアクセス許可 ] を 選択します。 [アクセス 許可の追加] を選択します。
[Microsoft Graph] を選択し、[委任されたアクセス許可] を選択します。 Mail.Read を追加し、[ アクセス許可の 追加] を選択します。

[管理 ] で [API を公開****する] を選択 し、[スコープ の追加] を選択します。
既定のアプリケーション ID URI を受け入れ、[ 保存して 続行] を選択します。
[スコープの追加 ] フォームに次のように 入力します。
- スコープ名: Mail.Read
- Who同意できますか: 管理者とユーザー
- 管理者の同意表示名: すべてのユーザーの受信トレイを読み取る
- 管理者の同意の説明: アプリがすべてのユーザーの受信トレイを読み取る
- ユーザーの同意表示名: 受信トレイの読み取り
- ユーザーの同意の説明: アプリで受信トレイの読み取りを許可する
- [状態]: 有効
[スコープの追加] を選択します。
新しいスコープをコピーします。後の手順で必要になります。

[管理 ] で [マニフェスト ] を 選択します。
マニフェスト
knownClientApplications内を見つけて、TEST_APP_ID[][TEST_APP_ID]現在の値を 、Azure Function Test App アプリ登録のアプリケーション ID Graph置き換える。 [保存] を選択します。
注意
テスト アプリケーションのアプリ ID knownClientApplications を Azure Function のマニフェストのプロパティに追加すると、テスト アプリケーションは結合された同意フロー をトリガーできます。 これは、代理フローが機能するために必要です。
Azure Function スコープを追加してアプリケーション登録をテストする
Azure 関数テスト アプリ Graphに 戻り、[管理] で [API のアクセス許可] を****選択します。 [アクセス許可を追加] を選択します。
[自分 の API] を選択 し、[その他の読み込 み] を選択します。 [Azure Graph] を選択します。
![[API アクセス許可の要求] ダイアログ のスクリーンショット](azure-functions/tutorial/images/test-app-add-permissions.png)
[ Mail.Read] アクセス許可を選択 し、[アクセス許可の 追加] を選択します。
[構成 済み アクセス許可] で、Microsoft Graph の [User.Read] アクセス許可を削除するには、アクセス許可の右側にある ... を選択し、[アクセス許可の削除] を選択します。 [ はい、削除] を選択 して確認します。

Azure Function webhook のアプリを登録する
[アプリの 登録] に戻り、[ 新しい登録] を選択します。 [アプリケーションを登録] ページで、次のように値を設定します。
Graph Azure Function Webhookに [名前] を設定します。- [サポート されているアカウントの種類] を [ この組織ディレクトリのアカウントのみ] に設定します。
- リダイレクト URI は空白 のままにします。
[登録] を選択します。 [Azure 関数Graph Webhook] ページで、アプリケーション (クライアント) ID の値をコピーして保存します。次の手順で必要になります。
[管理] で [証明書とシークレット] を選択します。 [新しいクライアント シークレット] ボタンを選択します。 [説明] に値を入力して、[有効期限] のオプションのいずれかを選び、[追加] を選択します。
クライアント シークレットの値をコピーしてから、このページから移動します。 次の手順で行います。
[管理 ] で [API のアクセス許可 ] を 選択します。 [アクセス 許可の追加] を選択します。
[Microsoft Graph] 、[ アプリケーションのアクセス許可 ] の順に選択します。 User.Read.All と Mail.Read を 追加し、[アクセス許可の追加 ] を選択します。
[構成 済み アクセス許可] で、権限の右側にある ... を選択し、[アクセス許可の削除] を選択して、Microsoft Graph の下で委任された User.Read アクセス許可を 削除します。 [ はい、削除] を選択 して確認します。
[管理者 の同意を許可する....] ボタンを選択し、[ は い] を選択して、構成されたアプリケーションのアクセス許可に対する管理者の同意を付与します。 [ 構成済み アクセス許可] テーブルの [ 状態] 列が [ ...] に変更されます。

認証に代わって API を実装する
この演習では、Azure Function GetMyNewestMessage の実装を完了し、テスト クライアントを更新して関数を呼び出します。
Azure 関数は、 代理フローを使用します。 このフローのイベントの基本的な順序は次のとおりです。
- テスト アプリケーションでは、対話型の認証フローを使用して、ユーザーがサインインして同意を付与できます。 Azure 関数にスコープが設定されているトークンを取得します。 トークンには 、Microsoft のスコープがGraphではありません。
- テスト アプリケーションは Azure 関数を呼び出し、ヘッダーにアクセス トークンを送信
Authorizationします。 - Azure 関数はトークンを検証し、そのトークンを Microsoft のスコープを含む 2 番目のアクセス トークンGraphします。
- Azure 関数は、2 番目のアクセス Graphを使用して、ユーザーの代わりに Microsoft Graphを呼び出します。
重要
アプリケーション ID とシークレットをソースに格納しないようにするには、 .NET Secret Manager を使用してこれらの値を格納します。 シークレット マネージャーは開発のみを目的としますが、実稼働アプリでは、シークレットを格納するために信頼できるシークレット マネージャーを使用する必要があります。
単一ページ アプリケーションへの認証の追加
まず、SPA に認証を追加します。 これにより、アプリケーションは Azure 関数を呼び出すアクセスを許可するアクセス トークンを取得できます。 これは SPA なので、 PKCE で認証コード フローを使用します。
TestClient ディレクトリに新しいファイルを作成し、config.jsコードを 追加します。
const msalConfig = { auth: { clientId: 'YOUR_TEST_APP_APP_ID_HERE', authority: 'https://login.microsoftonline.com/YOUR_TENANT_ID_HERE' } }; const msalRequest = { // Scope of the Azure Function scopes: [ 'YOUR_AZURE_FUNCTION_APP_ID_HERE/.default' ] }Azure
YOUR_TEST_APP_APP_ID_HERE関数テスト アプリの Azure portal で作成したアプリケーション ID にGraph します。 AzureYOUR_TENANT_ID_HEREportal から コピーしたディレクトリ (テナント) ID 値に置き換えてください。 AzureYOUR_AZURE_FUNCTION_APP_ID_HERE関数のアプリケーション ID に置きGraphします。重要
git などのソース管理を使用している場合は、 config.js ファイルをソース管理から除外して、アプリ ID とテナント ID が誤って漏洩しないようにする良い時期になります。
TestClient ディレクトリに新しいファイルを作成し、auth.jsコードを 追加します。
// Create the main MSAL instance // configuration parameters are located in config.js const msalClient = new msal.PublicClientApplication(msalConfig); async function signIn() { // Login try { // Use MSAL to login const authResult = await msalClient.loginPopup(msalRequest); // Save the account username, needed for token acquisition sessionStorage.setItem('msal-userName', authResult.account.username); // Refresh home page updatePage(Views.home); } catch (error) { console.log(error); updatePage(Views.error, { message: 'Error logging in', debug: error }); } } function signOut() { account = null; sessionStorage.removeItem('msal-userName'); msalClient.logout(); }このコードの動作を検討します。
- このメソッドは、ファイルに格納
PublicClientApplicationされている値を使用して初期化 config.js。 - Azure 関数
loginPopupのアクセス許可スコープを使用して、ユーザーにサインインするために使用します。 - ユーザーのユーザー名がセッションに保存されます。
重要
アプリは使用しますの
loginPopupで、ブラウザーのポップアップ ブロックを変更してポップアップを許可する必要がある場合がありますhttp://localhost:8080。- このメソッドは、ファイルに格納
ページを更新してサインインします。 ページは、サインインが成功したことを示すユーザー名で更新する必要があります。
Azure 関数への認証の追加
このセクションでは、GetMyNewestMessageAzure 関数に代理フローを実装して、Microsoft のサービス と互換性のあるアクセス トークンを取得Graph。
.NET 開発シークレット ストアを初期化するには、 GraphTutorial.csproj を含むディレクトリで CLI を開き、次のコマンドを実行します。
dotnet user-secrets init次のコマンドを使用して、アプリケーション ID、シークレット、テナント ID をシークレット ストアに追加します。 Azure
YOUR_API_FUNCTION_APP_ID_HERE関数のアプリケーション ID に置きGraphします。 AzureYOUR_API_FUNCTION_APP_SECRET_HERE関数の Azure ポータルで作成したアプリケーション シークレットに置 きGraphします。 AzureYOUR_TENANT_ID_HEREportal から コピーしたディレクトリ (テナント) ID 値に置き換えてください。dotnet user-secrets set apiFunctionId "YOUR_API_FUNCTION_APP_ID_HERE" dotnet user-secrets set apiFunctionSecret "YOUR_API_FUNCTION_APP_SECRET_HERE" dotnet user-secrets set tenantId "YOUR_TENANT_ID_HERE"
受信ベアラー トークンの処理
このセクションでは、SPA から Azure 関数に送信されるベアラー トークンを検証および処理するクラスを実装します。
認証という名前の GraphTutorial ディレクトリに新しい ディレクトリを作成 します。
./GraphTutorial/Authentication フォルダーに TokenValidationResult.cs という名前の新しいファイルを作成し、次のコードを追加します。
namespace GraphTutorial.Authentication { public class TokenValidationResult { // MSAL account ID - used to access the token // cache public string MsalAccountId { get; private set; } // The extracted token - used to build user assertion // for OBO flow public string Token { get; private set; } public TokenValidationResult(string msalAccountId, string token) { MsalAccountId = msalAccountId; Token = token; } } }./GraphTutorial/Authentication フォルダーに TokenValidation.cs という名前の新しいファイルを作成し、次のコードを追加します。
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using System; using System.Security.Claims; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public static class TokenValidation { private static TokenValidationParameters _validationParameters = null; public static async Task<TokenValidationResult> ValidateAuthorizationHeader( HttpRequest request, string tenantId, string expectedAudience, ILogger log) { // Check for Authorization header if (request.Headers.ContainsKey("authorization")) { var authHeader = AuthenticationHeaderValue.Parse(request.Headers["authorization"]); if (authHeader != null && authHeader.Scheme.ToLower() == "bearer" && !string.IsNullOrEmpty(authHeader.Parameter)) { if (_validationParameters == null) { // Load the tenant-specific OpenID config from Azure var configManager = new ConfigurationManager<OpenIdConnectConfiguration>( $"https://login.microsoftonline.com/{tenantId}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); var config = await configManager.GetConfigurationAsync(); _validationParameters = new TokenValidationParameters { // Use signing keys retrieved from Azure IssuerSigningKeys = config.SigningKeys, ValidateAudience = true, // Audience MUST be the app ID for the Web API ValidAudience = expectedAudience, ValidateIssuer = true, // Use the issuer retrieved from Azure ValidIssuer = config.Issuer, ValidateLifetime = true }; } var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken jwtToken; try { // Validate the token var result = tokenHandler.ValidateToken(authHeader.Parameter, _validationParameters, out jwtToken); // If ValidateToken did not throw an exception, token is valid. return new TokenValidationResult(GetMsalAccountId(result), authHeader.Parameter); } catch (Exception exception) { log.LogError(exception, "Error validating bearer token"); } } } return null; } // Helper function to construct an MSAL account ID from the // claims in the token. MSAL uses an ID in the format // oid.tid, where oid is the object ID of the user, and tid is // the tenant ID. private static string GetMsalAccountId(ClaimsPrincipal principal) { var objectId = principal?.FindFirst("oid"); if (objectId == null) { objectId = principal?.FindFirst( "http://schemas.microsoft.com/identity/claims/objectidentifier"); } var tenantId = principal?.FindFirst("tid"); if (tenantId == null) { tenantId = principal?.FindFirst( "http://schemas.microsoft.com/identity/claims/tenantid"); } if (objectId != null && tenantId != null) { return $"{objectId.Value}.{tenantId.Value}"; } return null; } } }
このコードの動作を検討します。
- ヘッダーにベアラー トークンが含まれています
Authorization。 - Azure の公開された OpenID 構成から署名と発行者を検証します。
- 対象ユーザー (
audクレーム) が Azure Function のアプリケーション ID と一致する検証を行います。 - トークンを解析し、MSAL アカウント ID を生成します。これは、トークンキャッシュを利用するために必要になります。
代理認証プロバイダーの作成
OnBehalfOfAuthProvider.cs という名前の認証ディレクトリに新しいファイルを作成し、そのファイルに次のコードを追加します。
using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Client; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public class OnBehalfOfAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private TokenValidationResult _tokenResult; private string[] _scopes; private ILogger _logger; public OnBehalfOfAuthProvider( IConfidentialClientApplication msalClient, TokenValidationResult tokenResult, string[] scopes, ILogger logger) { _scopes = scopes; _logger = logger; _tokenResult = tokenResult; _msalClient = msalClient; } public async Task<string> GetAccessToken() { try { // First attempt to get token from the cache for this user // Check for a matching account in the cache var account = await _msalClient.GetAccountAsync(_tokenResult.MsalAccountId); if (account != null) { // Make a "silent" request for a token. This will // return the cached token if still valid, and will handle // refreshing the token if needed var cacheResult = await _msalClient .AcquireTokenSilent(_scopes, account) .ExecuteAsync(); _logger.LogInformation($"User access token: {cacheResult.AccessToken}"); return cacheResult.AccessToken; } } catch (MsalUiRequiredException) { // This exception indicates that a new token // can only be obtained by invoking the on-behalf-of // flow. "UiRequired" isn't really accurate since the OBO // flow doesn't involve UI. // Catching the exception so code will continue to the // AcquireTokenOnBehalfOf call below. } catch (Exception exception) { _logger.LogError(exception, "Error getting access token via on-behalf-of flow"); return null; } try { _logger.LogInformation("Token not found in cache, attempting OBO flow"); // Use the token sent by the calling client as a // user assertion var userAssertion = new UserAssertion(_tokenResult.Token); // Invoke on-behalf-of flow var result = await _msalClient .AcquireTokenOnBehalfOf(_scopes, userAssertion) .ExecuteAsync(); _logger.LogInformation($"User access token: {result.AccessToken}"); return result.AccessToken; } catch (Exception exception) { _logger.LogError(exception, "Error getting access token from cache"); return null; } } // This is the delegate called by the GraphServiceClient on each // request. public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { // Get the current access token var token = await GetAccessToken(); // Add the token in the Authorization header requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } } }
OnBehalfOfAuthProvider.cs のコードが何を行うのかを少し考えます。
- 関数では
GetAccessToken、まず、 を使用してトークン キャッシュからユーザー トークンを取得しますAcquireTokenSilent。 これが失敗した場合は、テスト アプリから Azure 関数に送信されたベアラー トークンを使用して、ユーザー アサーションを生成します。 その後、そのユーザー アサーションを使用して、Graph互換性のあるトークンを取得しますAcquireTokenOnBehalfOf。 - インターフェイスを実装
Microsoft.Graph.IAuthenticationProviderし、このクラスGraphServiceClientを送信要求を認証するためのコンストラクターで渡すことができます。
クライアント サービスGraph実装する
このセクションでは、依存関係の挿入用に登録できるサービス を実装します。 このサービスは、認証されたクライアントを取得Graphされます。
Services という名前の GraphTutorial ディレクトリに新しい ディレクトリを作成 します。
IGraphClientService.cs という名前の Services ディレクトリに新しいファイルを作成し、そのファイルに次のコードを追加します。
using GraphTutorial.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Graph; namespace GraphTutorial.Services { public interface IGraphClientService { GraphServiceClient GetUserGraphClient( TokenValidationResult validation, string[] scopes, ILogger logger); GraphServiceClient GetAppGraphClient(ILogger logger); } }GraphClientService.cs という名前の Services ディレクトリに新しいファイルを作成し、そのファイルに次のコードを追加します。
using GraphTutorial.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Graph; namespace GraphTutorial.Services { // Service added via dependency injection // Used to get an authenticated Graph client public class GraphClientService : IGraphClientService { } }クラスに次のプロパティを追加
GraphClientServiceします。// Configuration private IConfiguration _config; // Single MSAL client object used for all user-related // requests. Making this a "singleton" here because the sample // uses the default in-memory token cache. private IConfidentialClientApplication _userMsalClient;クラスに次の関数を追加
GraphClientServiceします。public GraphClientService(IConfiguration config) { _config = config; } public GraphServiceClient GetUserGraphClient(TokenValidationResult validation, string[] scopes, ILogger logger) { // Only create the MSAL client once if (_userMsalClient == null) { _userMsalClient = ConfidentialClientApplicationBuilder .Create(_config["apiFunctionId"]) .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true) .WithTenantId(_config["tenantId"]) .WithClientSecret(_config["apiFunctionSecret"]) .Build(); } // Create a new OBO auth provider for the specific user var authProvider = new OnBehalfOfAuthProvider(_userMsalClient, validation, scopes, logger); // Return a GraphServiceClient initialized with the auth provider return new GraphServiceClient(authProvider); }関数のプレースホルダー実装を追加
GetAppGraphClientします。 後のセクションで実装します。public GraphServiceClient GetAppGraphClient() { throw new System.NotImplementedException(); }この
GetUserGraphClient関数は、トークン検証の結果を取得し、ユーザーの認証をGraphServiceClient構築します。./GraphTutorial/Program.cs を開き、その内容を次に置き換えてください。
このコードは、構成にユーザー シークレットを追加し、Azure Functions で依存関係の挿入を有効にし、サービスを公開
GraphClientServiceします。
GetMyNewestMessage 関数を実装する
./GraphTutorial/GetMyNewestMessage.cs を開き、その内容全体を次に置き換えてください。
using GraphTutorial.Authentication; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class GetMyNewestMessage { private IConfiguration _config; private IGraphClientService _clientService; public GetMyNewestMessage(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("GetMyNewestMessage")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["apiFunctionId"]) || string.IsNullOrEmpty(_config["apiFunctionSecret"]) || string.IsNullOrEmpty(_config["tenantId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } // Validate the bearer token var validationResult = await TokenValidation.ValidateAuthorizationHeader( req, _config["tenantId"], _config["apiFunctionId"], log); // If token wasn't returned it isn't valid if (validationResult == null) { return new UnauthorizedResult(); } // Initialize a Graph client for this user var graphClient = _clientService.GetUserGraphClient(validationResult, new[] { "https://graph.microsoft.com/.default" }, log); // Get the user's newest message in inbox // GET /me/mailfolders/inbox/messages var messagePage = await graphClient.Me .MailFolders .Inbox .Messages .Request() // Limit the fields returned .Select(m => new { m.From, m.ReceivedDateTime, m.Subject }) // Sort by received time, newest on top .OrderBy("receivedDateTime DESC") // Only get back one message .Top(1) .GetAsync(); if (messagePage.CurrentPage.Count < 1) { return new OkObjectResult(null); } // Return the message in the response return new OkObjectResult(messagePage.CurrentPage[0]); } } }
GetMyNewestMessage.cs のコードを確認する
GetMyNewestMessage.cs のコードが何を行うのかを少し考えます。
- コンストラクターでは、依存関係の挿入によって
IConfiguration渡されたIGraphClientServiceオブジェクトとオブジェクトを保存します。 - 関数では
Run、次の手順を実行します。- オブジェクトに存在する必要な構成値を検証
IConfigurationします。 - ベアラー トークンを検証し、トークンが
401無効な場合は状態コードを返します。 - この要求Graphユーザーの
GraphClientServiceクライアントを取得します。 - Microsoft Graph SDK を使用して、ユーザーの受信トレイから最新のメッセージを取得し、応答で JSON 本文として返します。
- オブジェクトに存在する必要な構成値を検証
テスト アプリから Azure 関数を呼び出す
[ auth.jsを開 き、次の関数を追加してアクセス トークンを取得します。
async function getToken() { let account = sessionStorage.getItem('msal-userName'); if (!account){ throw new Error( 'User account missing from session. Please sign out and sign in again.'); } try { // First, attempt to get the token silently const silentRequest = { scopes: msalRequest.scopes, account: msalClient.getAccountByUsername(account) }; const silentResult = await msalClient.acquireTokenSilent(silentRequest); return silentResult.accessToken; } catch (silentError) { // If silent requests fails with InteractionRequiredAuthError, // attempt to get the token interactively if (silentError instanceof msal.InteractionRequiredAuthError) { const interactiveResult = await msalClient.acquireTokenPopup(msalRequest); return interactiveResult.accessToken; } else { throw silentError; } } }このコードの動作を検討します。
- 最初に、ユーザーの操作なしで、アクセス トークンをサイレント モードで取得します。 ユーザーは既にサインインしている必要があるから、MSAL はキャッシュ内のユーザーのトークンを持つ必要があります。
- ユーザーが対話する必要があるというエラーで失敗した場合は、対話的にトークンを取得します。
ヒント
アクセス トークンhttps://jwt.ms
audを解析して、クレームが Azure 関数scpのアプリ ID であり、クレームに Microsoft Graph ではなく Azure 関数のアクセス許可スコープが含まれているか確認できます。TestClient ディレクトリに新しいファイルを作成し、azurefunctions.jsコードを 追加します。
async function getLatestMessage() { const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } try { const response = await fetch('http://localhost:7071/api/GetMyNewestMessage', { headers: { Authorization: `Bearer ${token}` } }); const message = await response.json(); updatePage(Views.message, message); } catch (error) { updatePage(Views.error, { message: 'Error getting message', debug: error }); } }CLI の現在のディレクトリを ./GraphTutorial ディレクトリに変更し、次のコマンドを実行して Azure 関数をローカルで開始します。
func startSPA をまだ提供していない場合は、2 番目の CLI ウィンドウを開き、現在のディレクトリ を ./TestClient ディレクトリに変更 します。 次のコマンドを実行して、テスト アプリケーションを実行します。
dotnet serve -h "Cache-Control: no-cache, no-store, must-revalidate"ブラウザーを開き、
http://localhost:8080に移動します。 サインインして、[最新のメッセージ] ナビゲーション アイテムを 選択します。 アプリは、ユーザーの受信トレイに最新のメッセージに関する情報を表示します。
クライアント資格情報認証を使用して Webhook を実装する
この演習では、Azure Functions SetSubscription Notifyの実装を完了し、テスト アプリケーションを更新して、ユーザーの受信トレイの変更をサブスクライブおよびサブスクライブ解除します。
- この
SetSubscription関数は API として機能し、テスト アプリはユーザーの受信トレイの変更に対するサブスクリプションを作成または削除できます。 - この
Notify関数は、サブスクリプションによって生成された変更通知を受け取る Webhook として機能します。
どちらの関数も、クライアント資格情報の付与フローを使用して、アプリ専用トークンを取得して Microsoft 資格情報を呼び出Graph。 管理者が必要なアクセス許可スコープに対する同意を管理者に与えたため、トークンを取得するためにユーザーの操作は必要ありません。
Azure Functions プロジェクトにクライアント資格情報認証を追加する
このセクションでは、Azure Functions プロジェクトにクライアント資格情報フローを実装して、Microsoft の資格情報と互換性のあるアクセス トークンを取得Graph。
GraphTutorial.csproj を含むディレクトリで CLI を開きます。
次のコマンドを使用して、Webhook アプリケーション ID とシークレットをシークレット ストアに追加します。 Azure
YOUR_WEBHOOK_APP_ID_HEREFunction Webhook のアプリケーション ID Graph置き換える。 AzureYOUR_WEBHOOK_APP_SECRET_HEREFunction Webhook の Azure ポータルで作成したアプリケーション Graphに置き換える。dotnet user-secrets set webHookId "YOUR_WEBHOOK_APP_ID_HERE" dotnet user-secrets set webHookSecret "YOUR_WEBHOOK_APP_SECRET_HERE"
クライアント資格情報認証プロバイダーの作成
ClientCredentialsAuthProvider.cs という 名前の ./GraphTutorial/Authentication ディレクトリに新しいファイルを作成し、次のコードを追加します。
using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Client; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace GraphTutorial.Authentication { public class ClientCredentialsAuthProvider : IAuthenticationProvider { private IConfidentialClientApplication _msalClient; private string[] _scopes; private ILogger _logger; public ClientCredentialsAuthProvider( string appId, string clientSecret, string tenantId, string[] scopes, ILogger logger) { _scopes = scopes; _logger = logger; _msalClient = ConfidentialClientApplicationBuilder .Create(appId) .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true) .WithTenantId(tenantId) .WithClientSecret(clientSecret) .Build(); } public async Task<string> GetAccessToken() { try { // Invoke client credentials flow // NOTE: This will return a cached token if a valid one // exists var result = await _msalClient .AcquireTokenForClient(_scopes) .ExecuteAsync(); _logger.LogInformation($"App-only access token: {result.AccessToken}"); return result.AccessToken; } catch (Exception exception) { _logger.LogError(exception, "Error getting access token via client credentials flow"); return null; } } // This is the delegate called by the GraphServiceClient on each // request. public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage) { // Get the current access token var token = await GetAccessToken(); // Add the token in the Authorization header requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); } } }
ClientCredentialsAuthProvider.cs のコードが何を行うのかを少し考えます。
- コンストラクターでは、パッケージから ConfidentialClientApplication を 初期化
Microsoft.Identity.Clientします。 and 関数をWithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)使用して.WithTenantId(tenantId)、ログイン対象ユーザーを指定したユーザーと組織Microsoft 365します。 - 関数では
GetAccessToken、アプリケーションのトークンAcquireTokenForClientを取得するために呼び出されます。 クライアント資格情報トークン フローは常に非対話型です。 - インターフェイスを実装
Microsoft.Graph.IAuthenticationProviderし、このクラスGraphServiceClientを送信要求を認証するためのコンストラクターで渡すことができます。
GraphClientService の更新
GraphClientService.cs を 開き、次のプロパティをクラスに追加します。
private GraphServiceClient _appGraphClient;既存の
GetAppGraphClient関数を、以下の関数で置き換えます。public GraphServiceClient GetAppGraphClient(ILogger logger) { if (_appGraphClient == null) { // Create a client credentials auth provider var authProvider = new ClientCredentialsAuthProvider( _config["webHookId"], _config["webHookSecret"], _config["tenantId"], // The https://graph.microsoft.com/.default scope // is required for client credentials. It requests // all of the permissions that are explicitly set on // the app registration new[] { "https://graph.microsoft.com/.default" }, logger); _appGraphClient = new GraphServiceClient(authProvider); } return _appGraphClient; }
Notify 関数の実装
このセクションでは、変更通知の Notify 通知 URL として使用される関数を実装します。
Models という名前の GraphTutorials ディレクトリに新しい ディレクトリを作成 します。
ResourceData.cs という名前の Models ディレクトリに新しいファイルを作成し、次のコードを追加します。
namespace GraphTutorial.Models { // Class to represent the resourceData object // inside a change notification public class ResourceData { public string Id { get;set; } } }ChangeNotificationPayload.cs という名前の Models ディレクトリに新しいファイルを作成し、次のコードを追加します。
NotificationList.cs という名前の Models ディレクトリに新しいファイルを作成し、次のコードを追加します。
namespace GraphTutorial.Models { // Class representing an array of notifications // in a notification payload public class NotificationList { public ChangeNotification[] Value { get;set; } } }./GraphTutorial/Notify.cs を開き、その内容全体を次に置き換えてください。
using GraphTutorial.Models; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System.IO; using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class Notify { public static readonly string ClientState = "GraphTutorialState"; private IConfiguration _config; private IGraphClientService _clientService; public Notify(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("Notify")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["webHookId"]) || string.IsNullOrEmpty(_config["webHookSecret"]) || string.IsNullOrEmpty(_config["tenantId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } // Is this a validation request? // https://docs.microsoft.com/graph/webhooks#notification-endpoint-validation string validationToken = req.Query["validationToken"]; if (!string.IsNullOrEmpty(validationToken)) { // Because validationToken is a string, OkObjectResult // will return a text/plain response body, which is // required for validation return new OkObjectResult(validationToken); } // Not a validation request, process the body var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); log.LogInformation($"Change notification payload: {requestBody}"); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Deserialize the JSON payload into a list of ChangeNotification // objects var notifications = JsonSerializer.Deserialize<NotificationList>(requestBody, jsonOptions); foreach (var notification in notifications.Value) { if (notification.ClientState == ClientState) { // Process each notification await ProcessNotification(notification, log); } else { log.LogInformation($"Notification received with unexpected client state: {notification.ClientState}"); } } // Return 202 per docs return new AcceptedResult(); } private async Task ProcessNotification(ChangeNotification notification, ILogger log) { var graphClient = _clientService.GetAppGraphClient(log); // The resource field in the notification has the URL to the // message, including the user ID and message ID. Since we // have the URL, use a MessageRequestBuilder instead of the fluent // API var msgRequestBuilder = new MessageRequestBuilder( $"https://graph.microsoft.com/v1.0/{notification.Resource}", graphClient); var message = await msgRequestBuilder.Request() .Select(m => new { m.Subject }) .GetAsync(); log.LogInformation($"The following message was {notification.ChangeType}:"); log.LogInformation($"Subject: {message.Subject}, ID: {message.Id}"); } } }
Notify.cs のコードが何を行うのかを少し考えます。
- この
Run関数は、クエリ パラメーターの有無をvalidationTokenチェックします。 このパラメーターが存在する場合、要求は検証要求として 処理され、それに応じて応答します。 - 要求が検証要求ではない場合、JSON ペイロードはに逆シリアル化されます
ChangeNotificationCollection。 - リスト内の各通知は、予期されるクライアント状態の値がチェックされ、処理されます。
- 通知をトリガーしたメッセージは、Microsoft サーバーで取得Graph。
SetSubscription 関数の実装
このセクションでは、SetSubscription 関数を実装します。 この関数は、ユーザーの受信トレイでサブスクリプションを作成または削除するためにテスト アプリケーションによって呼び出される API として機能します。
SetSubscriptionPayload.cs という名前の Models ディレクトリに新しいファイルを作成し、次のコードを追加します。
namespace GraphTutorial.Models { // Class to represent the payload sent to the // SetSubscription function public class SetSubscriptionPayload { // "subscribe" or "unsubscribe" public string RequestType { get;set; } // If unsubscribing, the subscription to delete public string SubscriptionId { get;set; } // If subscribing, the user ID to subscribe to // Can be object ID of user, or userPrincipalName public string UserId { get;set; } } }./GraphTutorial/SetSubscription.cs を開き、その内容全体を次に置き換えてください。
using GraphTutorial.Authentication; using GraphTutorial.Models; using GraphTutorial.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Graph; using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; using System.Web.Http; namespace GraphTutorial { public class SetSubscription { private IConfiguration _config; private IGraphClientService _clientService; public SetSubscription(IConfiguration config, IGraphClientService clientService) { _config = config; _clientService = clientService; } [FunctionName("SetSubscription")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log) { // Check configuration if (string.IsNullOrEmpty(_config["webHookId"]) || string.IsNullOrEmpty(_config["webHookSecret"]) || string.IsNullOrEmpty(_config["tenantId"]) || string.IsNullOrEmpty(_config["apiFunctionId"])) { log.LogError("Invalid app settings configured"); return new InternalServerErrorResult(); } var notificationHost = _config["ngrokUrl"]; if (string.IsNullOrEmpty(notificationHost)) { notificationHost = req.Host.Value; } // Validate the bearer token var validationResult = await TokenValidation.ValidateAuthorizationHeader( req, _config["tenantId"], _config["apiFunctionId"], log); // If token wasn't returned it isn't valid if (validationResult == null) { return new UnauthorizedResult(); } var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; // Deserialize the JSON payload into a SetSubscriptionPayload object var payload = JsonSerializer.Deserialize<SetSubscriptionPayload>(requestBody, jsonOptions); if (payload == null) { return new BadRequestErrorMessageResult("Invalid request payload"); } // Initialize Graph client var graphClient = _clientService.GetAppGraphClient(log); if (payload.RequestType.ToLower() == "subscribe") { if (string.IsNullOrEmpty(payload.UserId)) { return new BadRequestErrorMessageResult("Required fields in payload missing"); } // Create a new subscription object var subscription = new Subscription { ChangeType = "created,updated", NotificationUrl = $"{notificationHost}/api/Notify", Resource = $"/users/{payload.UserId}/mailfolders/inbox/messages", ExpirationDateTime = DateTimeOffset.UtcNow.AddDays(2), ClientState = Notify.ClientState }; // POST /subscriptions var createdSubscription = await graphClient.Subscriptions .Request() .AddAsync(subscription); return new OkObjectResult(createdSubscription); } else { if (string.IsNullOrEmpty(payload.SubscriptionId)) { return new BadRequestErrorMessageResult("Subscription ID missing in payload"); } // DELETE /subscriptions/subscriptionId await graphClient.Subscriptions[payload.SubscriptionId] .Request() .DeleteAsync(); return new AcceptedResult(); } } } }
SetSubscription.cs のコードが何を行うのかを少し考えます。
- この
Run関数は、POST 要求で送信された JSON ペイロードを読み取り、要求の種類 (サブスクライブまたはサブスクライブ解除)、サブスクライブするユーザー ID、購読解除するサブスクリプション ID を決定します。 - 要求がサブスクライブ要求の場合、Microsoft Graph SDK を使用して、指定したユーザーの受信トレイに新しいサブスクリプションを作成します。 サブスクリプションは、メッセージが作成または更新されると通知されます。 新しいサブスクリプションは、応答の JSON ペイロードに返されます。
- 要求が購読解除要求の場合は、Microsoft Graph SDK を使用して、指定したサブスクリプションを削除します。
テスト アプリから SetSubscription を呼び出す
このセクションでは、テスト アプリでサブスクリプションを作成および削除する関数を実装します。
./TestClient/azurefunctions.jsを開き、次の関数を追加します。
async function createSubscription() { // Get the user to subscribe for const userId = document.getElementById('subscribe-user').value; if (!userId) { updatePage(Views.error, { message: 'Please provide a user ID or userPrincipalName' }); return; } const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } // Build the JSON payload for the subscribe request const payload = { requestType: 'subscribe', userId: userId }; const response = await fetch('http://localhost:7071/api/SetSubscription', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(payload) }); if (response.ok) { // Get the new subscription from the response const subscription = await response.json(); // Add the new subscription to the array of subscriptions // in the session let existingSubscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')) || []; existingSubscriptions.push({ userId: userId, subscriptionId: subscription.id }); sessionStorage.setItem('graph-subscriptions', JSON.stringify(existingSubscriptions)); // Refresh the subscriptions page to display the new // subscription updatePage(Views.subscriptions); return; } updatePage(Views.error, { message: `Call to SetSubscription returned ${response.status}`, debug: response.statusText }); }このコードは、Azure 関数を
SetSubscription呼び出してサブスクライブし、新しいサブスクリプションをセッション内のサブスクリプションの配列に追加します。次の関数を次の関数 にazurefunctions.js します。
async function deleteSubscription(subscriptionId) { const token = await getToken(); if (!token) { updatePage(Views.error, { message: 'Could not retrieve token for user' }); return; } // Build the JSON payload for the unsubscribe request const payload = { requestType: 'unsubscribe', subscriptionId: subscriptionId }; const response = await fetch('http://localhost:7071/api/SetSubscription', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: JSON.stringify(payload) }); if (response.ok) { // Remove the subscription from the array let existingSubscriptions = JSON.parse(sessionStorage.getItem('graph-subscriptions')) || []; const subscriptionIndex = existingSubscriptions.findIndex((item) => { return item.subscriptionId === subscriptionId; }); existingSubscriptions.splice(subscriptionIndex, 1); sessionStorage.setItem('graph-subscriptions', JSON.stringify(existingSubscriptions)); // Refresh the subscriptions page updatePage(Views.subscriptions); return; } updatePage(Views.error, { message: `Call to SetSubscription returned ${response.status}`, debug: response.statusText }); }このコードは、Azure 関数を
SetSubscription呼び出してサブスクリプションをサブスクライブ解除し、セッション内のサブスクリプションの配列から削除します。ngrok を実行していない場合は、ngrok (
ngrok http 7071) を実行し、HTTPS 転送 URL をコピーします。次のコマンドを実行して、ngrok URL をユーザー シークレット ストアに追加します。
dotnet user-secrets set ngrokUrl "YOUR_NGROK_URL_HERE"重要
ngrok を再起動する場合は、このコマンドを繰り返して ngrok URL を更新する必要があります。
CLI の現在のディレクトリを ./GraphTutorial ディレクトリに変更し、次のコマンドを実行して Azure 関数をローカルで開始します。
func startSPA を更新し、[ サブスクリプション] ナビゲーション アイテムを 選択します。 メールボックスを持つ組織のユーザー Microsoft 365 ID をExchange Onlineします。 これは、ユーザー
idの (Microsoft Graph) か、ユーザーのいずれかですuserPrincipalName。 [サブスクライブ ] をクリックします。新しいサブスクリプションを表に表示するページが更新されます。
ユーザーに電子メールを送信します。 しばらくすると、関数
Notifyを呼び出す必要があります。 これを確認するには、ngrok Web インターフェイス (http://localhost:4040) または Azure Function プロジェクトのデバッグ出力を使用します。... [7/8/2020 7:33:57 PM] The following message was created: [7/8/2020 7:33:57 PM] Subject: Hi Megan!, ID: AAMkAGUyN2I4N2RlLTEzMTAtNDBmYy1hODdlLTY2NTQwODE2MGEwZgBGAAAAAAA2J9QH-DvMRK3pBt_8rA6nBwCuPIFjbMEkToHcVnQirM5qAAAAAAEMAACuPIFjbMEkToHcVnQirM5qAACHmpAsAAA= [7/8/2020 7:33:57 PM] Executed 'Notify' (Succeeded, Id=9c40af0b-e082-4418-aa3a-aee624f30e7a) ...テスト アプリで、サブスクリプションの テーブル行 の [削除] をクリックします。 ページが更新され、サブスクリプションがテーブルに表示されなくなりました。
Azure への発行の準備
この演習では、Azure Functions アプリへの発行の準備に必要な Azure 関数のサンプルに必要な変更 について説明します。
コードを更新する
構成はユーザー シークレット ストアから読み取り、開発マシンにのみ適用されます。 Azure に発行する前に、構成の保存場所を変更し、 Program.cs のコードを適切 に更新する必要があります。
アプリケーション シークレットは、Azure Key Vault などのセキュリティで保護された ストレージに格納する必要があります。
Azure 関数の CORS 設定を更新する
このサンプルでは、テスト アプリケーションが関数を呼び出すのを許可するように local.settings.json で CORS を構成しました。 発行された関数を構成して、それを呼び出す SPA アプリを許可する必要があります。
アプリの登録を更新する
azure knownClientApplications Function アプリの登録Graphマニフェスト内のプロパティは、Azure 関数 を呼び出すアプリのアプリケーションの ID を使用して更新する必要があります。
既存のサブスクリプションを再作成する
ローカル コンピューターまたは ngrok の webhook URL を使用して作成されたサブスクリプションは、Azure 関数の実稼働 URL Notify を使用して再作成する必要があります。
おめでとうございます。
Azure Functions Microsoft のチュートリアルをGraphしました。 Microsoft Graphを呼び出す作業アプリが作成されたので、新しい機能を試して追加できます。 Microsoft Graphの概要を参照して、Microsoft Graph でアクセスできるすべてのデータを確認Graph。
フィードバック
このチュートリアルに関するフィードバックは、リポジトリのGitHubしてください。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。