Bot Builder v4 でボット開発 : 他サービスに対する認証

ボットから Office 365 など他サービスを呼び出す際に、Azure AD など外部 ID サービスに対して認証が必要となります。Bot Service では認証を GUI での設定と簡単なコードで実現できます。

# Azure Bot Service が提供する認証支援機能

今までは、他サービスの認証を行う為には開発者がコードを全て用意する必要がありました。またセキュリティ対応のため、認証後にユーザーにマジックコード (6 桁の数字など) を別途送ってもらい認証を完了していましたが、Azure Bot Service ではその処理をより簡単にします。

- マジックコードを使わずに認証を行える
- ポータルから OAuth 対応プロバイダーの追加や削除など構成が可能
- Azure AD や GitHub など複数のプロバイダーをはじめから用意
- Bot Builder SDK でトークンの取得や認証ダイアログの作成などをサポート

# Azure AD に対する認証の準備

今回は ScheduleDialog 機能で Microsoft Graph を利用するため、Azure AD v2 の認証を設定します。Azure AD には v1 と v2 がありますが、詳しは[Microsoft Graph を使ってみよう : Azure AD における認証概要](https://qiita.com/kenakamu/items/3ea0a8d149eb44a9b08b) を参照してください。

## 前提

- Office 365 (管理者権限が必要のため、試用版で試すことを推奨します) 

※持っていない場合、[こちら](https://products.office.com/ja-jp/business/office-365-enterprise-e3-business-software) より体験版を作成してください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/87a6a8a3-e9a3-5ef8-c130-27e1292e1acd.png)

## Azure ポータルからアプリケーションの登録

まず Azure AD に対して Microsoft Graph を利用するアプリケーションを登録します。

1\. 利用している Office 365 の管理者アカウントで [Microsoft Application Registration Portal](https://apps.dev.microsoft.com/) にログイン。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4541ee57-3506-cbd9-72f2-9480fdeb7223.png)

2\. 「Add an app」ボタンをクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8a0be15d-0cba-2b4c-34c7-1f6c95842619.png)

3\. 名前を付けて、「Create」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/509ddea4-38f6-0f7b-a57a-445f29eb389a.png)

4\. Application Id をメモしてから、Application Secrets で「Generate New Password」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/be30b137-ab9f-3f0d-21bb-cc9ef46c5592.png)


5\. パスワードが表示されるのでコピー。※一度しか表示されない。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/70007564-2e63-5ef9-67e5-3a720d83e55f.png)

6\. Platform より「Add Platform」をクリック
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/35ab393f-9f0c-5b0f-f814-920b605816a7.png)

7\. 「Web」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9734c18e-4985-4356-1df4-791f26661831.png)

8\. Redirect URLs に 「https\://token.botframework.com/.auth/web/redirect」 を追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e6adc47a-b671-e2a1-74f9-333ace033657.png)

9\. Microsoft Graph Permission の Delegated Permissions より「Add」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e2145e21-5554-7fb4-34c0-2665b29eea7b.png)

10\. Calendars.Read にチェックを入れて「OK」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/241aea3f-a527-0a39-5b2e-34b67bae1410.png)

11\. 「Save」をクリックしてアプリケーションの登録完了。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/37af4c17-3cff-d54f-8b2c-2d19d94581ca.png)

## Bot チャンネルへの登録

次に Bot チャンネルに対して、作成したアプリケーションを構成します。

1\. [Azure ポータル](https://portal.azure.com) にログインし、「リソースの作成」をクリック。※ Office 365 ユーザーではなく、開発で利用している Azure アカウントを利用。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/866d5094-2d8b-9d92-f138-6ad5a4cf8914.png)

2\. 「Bot Channels Registration」を検索して作成。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f8196449-9a7f-e6ce-4e38-4966447bbfcb.png)

3\. 任意の名前を付けて「作成」をクリック。※価格レベルは F0  (無料) で今回は十分。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/fdd3da18-a770-e55b-f703-d163f57faa7b.png)

4\. 作成が完了したらリソースに移動して、「設定」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/bf7b0d86-2b0d-3e8f-51bf-1f875a4f6ef7.png)

5\. 画面一番下にある OAuth 接続設定より「設定の追加」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/cdcd64dd-5a9d-54f7-4af1-85aa7af7d635.png)

6\. 名前を付けて、サービスプロバイダーより「Azure Active Directory v2」を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/83ea03dd-400c-1bc8-d98b-411bed31d345.png)

7\. 画面の様に設定を追加して「Save」をクリック。
※ Tenant ID は Office 365 のテナント ID を入れるか、複数のテナントで使いたい場合は common を指定。
※スコープは大文字小文字を識別する。複数指定する場合は半角スペース
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9656617f-72e6-29c6-416f-36231b55805f.png)

8\. 作成された接続を開いて「接続テスト」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/63a36530-b210-8313-32e8-1bb9ab9c7842.png)

9\. Office 365 管理者権限でログイン。アプリ名、アクセス許可が正しいことを確認。「組織の代理として同意する」にチェックをいれて「承諾」をクリック。これにより他の一般ユーザーではコンセントが出なくて済む。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/3ac7d2ed-80db-0c0e-03ad-e9c335361c36.png)

10\. 成功すると以下の画面が出る。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/50551953-9d38-b515-3b7f-71af6e0113c4.png)

11\. 次にボットで使うための ApplicationId とパスワードを確認。構成にある「Microsoft App ID」をコピーして保存。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9bb8a4a3-bec1-6403-d02f-3a4fffc4456a.png)

12\. 管理リンクをクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/85103626-5aea-bda5-0032-07ac95e3caf9.png)

13\. Application Secret セクションで「Generate New Password」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/31d12d87-66b2-e538-465c-c24dea87d91e.png)

14\. 表示されたパスワードを保存しておく。

# Bot にログイン機能を追加

最後にコードを追加します。Microsoft.Bot.Builder.Dialogs モジュールで提供される OAuthPrompt を使って実装します。

1\. ScheduleDialog.cs を以下のコードと差し替え。

- OAuthPrompt の利用
- 取得したトークンの表示

```csharp
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

public class ScheduleDialog : ComponentDialog
{
    private const string connectionName = "AzureAdv2";
    public ScheduleDialog() : base(nameof(ScheduleDialog))
    {
        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            LoginAsync,
            LoginStepAsync,
        };

        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("schedule", waterfallSteps));
        AddDialog(new OAuthPrompt(
                "login",
                new OAuthPromptSettings
                {
                    ConnectionName = connectionName,
                    Text = "サインインダイアログ",
                    Title = "サインイン",
                    Timeout = 300000, // 5分でタイムアウトするように設定
                }));
    }

    private static async Task<DialogTurnResult> LoginAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        return await stepContext.BeginDialogAsync("login", cancellationToken: cancellationToken);
    }
    private static async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // ログインの結果よりトークンを取得
        var tokenResponse = (TokenResponse)stepContext.Result;
        
        await stepContext.Context.SendActivityAsync($"Token: {tokenResponse.Token}", cancellationToken: cancellationToken);
        return await stepContext.EndDialogAsync();
    }
}
```

2\. 上記で取得した AppId とパスワードを MyBot.bot に追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9c4b9c03-49f8-c82d-d703-d7807e62e882.png)

3\. Startup.cs の AddBot<MyBot> 内で認証情報を使うように設定。

```csharp
services.AddBot<MyBot>(options =>
{
    options.Middleware.Add(new MyLoggingMiddleware());
    options.Middleware.Add(new MyMiddleware());
                
    // Endpoint を構成ファイルより取得
    EndpointService endpointService = (EndpointService)botConfig.Services.Where(x => x.Type == "endpoint").First();
    // 認証として　AppId と AppPassword を使うように設定
    options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);

    // ストレージとしてインメモリを利用
    IStorage dataStore = new MemoryStorage();
    var userState = new UserState(dataStore);
    var conversationState = new ConversationState(dataStore);
    options.State.Add(userState);
    options.State.Add(conversationState);
});
```

4\. MyBot.cs のコンストラクタで ScheduleDialog を DialogSets に追加。

```csharp
public MyBot(MyStateAccessors accessors, LuisRecognizer luisRecognizer)
{
    this.accessors = accessors;
    this.luisRecognizer = luisRecognizer;
    this.dialogs = new DialogSet(accessors.ConversationDialogState);

    // コンポーネントダイアログを追加
    dialogs.Add(new ProfileDialog(accessors));
    dialogs.Add(new MenuDialog());
    dialogs.Add(new WeatherDialog());
    dialogs.Add(new ScheduleDialog());
}
```

5\. OnTurnAsync メソッドで LUIS の結果によって ScheduleDialog へ振り分けるよう、分岐を追加。

```csharp
else if (topIntent.Value.intent == "Schedule")
{
    await dialogContext.BeginDialogAsync(nameof(ScheduleDialog), null, cancellationToken);
}
```

6\. 認証後は OnTurnAsync に対して Activity.Type が Event または Invoke で戻ってくるため、処理を追加。

```csharp
else if(turnContext.Activity.Type == ActivityTypes.Event || turnContext.Activity.Type == ActivityTypes.Invoke)
{
    // Event または Invoke で戻ってきた場合ダイアログを続ける
    await dialogContext.ContinueDialogAsync(cancellationToken);
    if (!turnContext.Responded)
    {
        await turnContext.SendActivityAsync("わかりませんでした。全てキャンセルします。", cancellationToken: cancellationToken);
        await dialogContext.CancelAllDialogsAsync(cancellationToken);
        await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
    }
}
```

7\. [LUIS ポータル](https://www.luis.ai) より新しいインテントを追加。Train してから Publish。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/27d00f2b-323d-4bb3-8dd6-fa96fa5b255c.png)

## テスト

1\. F5 キーを押下してデバッグ実行後、エミュレーターを起動。新しい .bot ファイルを読み込み。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/3497509a-2d2b-c6cf-2c69-37835b1912b0.png)

2\. プロファイルダイアログを完了し、「予定を確認」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/0d9fff10-20ee-772e-32c1-4411c692f8d4.png)

3\. サインインプロンプトが出るので、「サインイン」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a079dcea-e26e-aa8e-eb53-a7a4a764a41e.png)

4\. ログイン画面が出るので、ログインを実行。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4cc0bf7c-b804-3267-43c6-302fdcb0bb98.png)

5\. サインイン完了後、トークンが取得できることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/20953a6b-14e5-2210-b8b8-4ddba022dd09.png)

6\. 再度「予定の確認」をクリックした際、ログインをしなくてもトークンが取れることを確認。

# Bot にログアウト機能を追加

一度ログインが成功すると、トークンの期限が切れるまでは再ログインを聞かれませんが、任意のタイミングでログアウトしたい場合もあるため、ここではログアウトを追加します。

1\. LUIS にログアウトインテントを追加。Train 後 Publish。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/d7d83e82-5b82-3439-8c30-11fc36caafde.png)

2\. MyBot.cs に新しくログアウトの判定と処理を追加。
※ここでは接続名を直接記述しているが Accessor に設定して取得する方がよい。

```csharp
else if (topIntent.Value.intent == "Logout")
{
    // アダプターを取得
    var botAdapter = (BotFrameworkAdapter)turnContext.Adapter;
    // 指定した接続をログアウト
    await botAdapter.SignOutUserAsync(turnContext, "AzureAdv2", cancellationToken: cancellationToken);
    await turnContext.SendActivityAsync("ログアウトしました。", cancellationToken: cancellationToken);
    var results = await dialogContext.ContinueDialogAsync(cancellationToken);

    // DialogTurnStatus が Complete または Empty の場合、メニューへ。
    if (results.Status == DialogTurnStatus.Complete || results.Status == DialogTurnStatus.Empty)          
    // メニューの表示
        await dialogContext.BeginDialogAsync(nameof(MenuDialog), null, cancellationToken);
}
```

## テスト

1\. F5 でデバッグを開始して、エミュレーターより「ログオフ」を送信。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/d1e71d88-3315-5913-e386-e5b29fff6ddd.png)

2\. 「予定の確認」を送信。再度サインインできることを確認。

# まとめ

今回は Azure AD v2 の認証について、ログイン/ログオフを見ていきました。次回は取得したトークンで Microsoft Graph から予定を取得します。

[次の記事へ](https://qiita.com/kenakamu/items/d6e05454d04d63904305)
[目次へ戻る](https://qiita.com/kenakamu/items/6dc043cfc1f199032883)

[この記事のサンプルコード](https://github.com/kenakamu/botbuilderv4completeguide/tree/article9)