【问题标题】:How can I continue an OAuth dialog after an invoke activity in a Microsoft Teams bot?如何在 Microsoft Teams 机器人中调用活动后继续 OAuth 对话框?
【发布时间】:2020-07-29 13:56:55
【问题描述】:

我有一个以 Microsoft Teams 为目标的机器人,主要是 1:1 主动聊天消息,因此它并不能真正管理对话方式。我正在尝试重构一些使用用户委托的应用程序权限的代码,所以我正在尝试实现here 描述的 OAuth 流程。

我已经从引用的示例中以几乎 1:1 的比例提升了身份验证对话框,开始时(带有派生自 ComponentDialog 的 OAuthPrompt 的基本瀑布对话框),我可以通过登录按钮获得 OAuth 提示,但我无法完成身份验证。这是一个代码sn-p:

    public class myBot : TeamsActivityHandler
    {
...
        public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
        {
...
            if (turnContext.Activity.Type == ActivityTypes.Message && turnContext.Activity.Text == "login")
            {
                var dialogState=_accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                var dialogSet = new DialogSet(dialogState);
                dialogSet.Add(new AuthDialog());
                DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                var turnResult=await dc.BeginDialogAsync("AuthDialog");
                await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
            }
            else if (turnContext.Activity.Type == ActivityTypes.Invoke && turnContext.Activity.Name == "signin/verifyState")
            {
                var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                var dialogSet = new DialogSet(dialogState);
                DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                var turnResult=await dc.ContinueDialogAsync();
                await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
            }


如果用户发送“登录”,我会按预期收到 OAuth 提示。单击登录按钮会生成弹出窗口,完成登录会将调用活动发送回机器人。问题出在处理登录/验证状态的块中。我可以获取 DialogContext,并尝试运行 ContinueDialog 以将控制权传递回 OAuthPrompt,但我收到异常消息“无法继续对话。找不到 id 为 AuthDialog 的对话”。问题是,如果我检查对话框上下文,我可以看到 dc.ActiveDialog.Id="AuthDialog",并且对话框在堆栈上。在这一点上我还需要做些什么来将控制权交还给对话框吗?

如果重要的话,这个机器人也在使用任务模块,所以我需要能够看到我从这些模块中获得的调用响应,这意味着我基本上是从 OnTurnAsync 调度所有内容。

【问题讨论】:

    标签: botframework microsoft-teams


    【解决方案1】:

    所以我完全不用对话框就可以进行身份​​验证,但是:

    一个。我不知道这是否是个好主意,因为所有示例都使用对话框,所以也许有一个很好的理由 湾。这还不是我在“生产”中拥有的东西,所以无论如何它都没有经过“实战测试”。

    也就是说,这基本上就是我正在做的事情,它将进入您的“signin/verifyState”代码分支:

    dynamic value = turnContext.Activity.Value;
    string magicCode = value.state;
    var adapter = (turnContext.Adapter as BotFrameworkAdapter);
    
        try
        {
            var connectionName = "[the name of your connection registered in the Azure Portal]";
            var token = await adapter.GetUserTokenAsync(turnContext, connectionName, magicCode, cancellationToken).ConfigureAwait(false);
        
            if (token != null)
            {
                // do something, like send the user a "thank you" message, or execute the desired command
            }
            else
            {
                await turnContext.SendActivityAsync(new Activity { Type = ActivityTypesEx.InvokeResponse, Value = new InvokeResponse { Status = 404 } }, cancellationToken).ConfigureAwait(false);
             }
    } catch { // todo: this doesn't seem to do anything...
        await turnContext.SendActivityAsync(new Activity { Type = ActivityTypesEx.InvokeResponse, Value = new InvokeResponse { Status = 500 } }, cancellationToken).ConfigureAwait(false);
    }
    

    【讨论】:

    • 谢谢-这确实有帮助。我可以使用它来执行“魔术代码”身份验证来获取令牌,并将其与 GetOauthSignInLinkAsync 结合起来手动管理流程。我确实喜欢对话版本将处理为用户返回现有令牌的方式,所以我仍然希望能够使用 OAuthPrompt 恢复对话。我只是想不通为什么在尝试恢复对话时会出现异常。
    【解决方案2】:

    我想我已经弄明白了,这更多是关于调整现有的不使用对话框的机器人来添加一个。首先,我从示例中添加了一个稍作修改的身份验证对话框:

        public class AuthDialog : ComponentDialog
        {
            public AuthDialog()
                : base(nameof(AuthDialog))
            {
                AddDialog(new OAuthPrompt(
                    nameof(OAuthPrompt),
                    new OAuthPromptSettings
                    {
                        ConnectionName = "botTeamsAuth",
                        Text = "Please Sign In",
                        Title = "Sign In",
                        Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
                    }));
    
                AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
    
                AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
                {
                    PromptStepAsync,
                    LoginStepAsync
                }));
    
                // The initial child Dialog to run.
                InitialDialogId = nameof(WaterfallDialog);
            }
    
            private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
            {
                return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
            }
    
            private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
            {
                var tokenResponse = (TokenResponse)stepContext.Result;
                if (tokenResponse?.Token != null)
                {
                    // do something with graph here
                    GraphServiceClient client = iceTeamsBotState.GetGraphClient(tokenResponse.Token);
                    var messages = await client.Me.MailFolders["inbox"].Messages.Request(new List<Microsoft.Graph.QueryOption> { new Microsoft.Graph.QueryOption("$search", $"\"subject:test\"") }).GetAsync();
                    await stepContext.Context.SendActivityAsync($"Found {messages.Count} emails");
                    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
                }
                await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
                return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
            }
        }
    

    然后,我需要改变我的机器人类:

        public class myBot : IBot
    

    到这里

        public class myBot<T> : TeamsActivityHandler where T:Dialog
    

    另外,我需要确保清单中的 validDomains 将“token.botframework.com”添加到列表中(我之前错过了这一步)。

    最后我在 ConfigureServices 中做了这个改变:

                //Add the auth dialog
                services.AddSingleton<AuthDialog>();
    
                services.AddBot<myBot<AuthDialog>>(options =>
                {
                    options.CredentialProvider = new SimpleCredentialProvider(botConfig.BotID, botConfig.BotSecret);
    
                    options.OnTurnError = async (context, exception) =>
                    {
                        _log.Error("Exception caught-OnTurnError: ", exception);
                        await context.SendActivityAsync("Sorry, it looks like something went wrong.");
                    };
                });
    

    注意将对话框注册为单例,然后将模板参数(可能代表主对话框)添加到机器人。然后,在我的 OnTurnAsync 代码中,我能够处理几个测试输入:

            public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
            {
    ...
                else if (turnContext.Activity.Type == ActivityTypes.Message && turnContext.Activity.Text == "logout")
                {
                    var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                    var dialogSet = new DialogSet(dialogState);
                    dialogSet.Add(new AuthDialog());
                    DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                    var botAdapter = (BotFrameworkAdapter)dc.Context.Adapter;
                    await botAdapter.SignOutUserAsync(dc.Context, "botTeamsAuth", null, cancellationToken);
                    await turnContext.SendActivityAsync($"logged out of graph");
                }
                else if (turnContext.Activity.Type == ActivityTypes.Message && turnContext.Activity.Text == "login")
                {
                    var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                    var dialogSet = new DialogSet(dialogState);
                    dialogSet.Add(new AuthDialog());
                    DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                    var turnResult = await dc.BeginDialogAsync("AuthDialog");
                    await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
                }
                else if (turnContext.Activity.Type == ActivityTypes.Invoke && turnContext.Activity.Name == "signin/verifyState")
                {
                    var dialogState = _accessors.ConversationState.CreateProperty<DialogState>(nameof(DialogState));
                    var dialogSet = new DialogSet(dialogState);
                    dialogSet.Add(new AuthDialog());  //this is counterintuitive, but it gets around the issue where I get the dialog missing exception
                    DialogContext dc = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
                    var turnResult = await dc.ContinueDialogAsync();
                    await _accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
                }
    

    是的,那里有一些重复的代码,但为了清楚起见,我暂时保持原样。至少对我来说似乎有点违反直觉的是更新 DialogState 和 DialogSet 以检索现有的状态成员,但这似乎是它的工作原理。在此示例中,我只是从用户收件箱中提取几条消息来验证连接,但它确实按预期工作。如果用户已经登录,则立即返回响应,如果用户需要进行身份验证,则发送登录卡并返回令牌响应。

    无论如何,我想我现在已经畅通无阻,希望这个解决方案可以帮助处于类似位置的其他人。如果您想使用“魔术代码”身份验证流程,@Hilton 在他的回答中提到的方法也将起作用。不过在那种情况下。你会调用这样的东西来获取登录链接:

                    var adapter = turnContext.Adapter as BotFrameworkAdapter;
                    string url = await adapter.GetOauthSignInLinkAsync(turnContext, "botTeamsAuth");
                    await turnContext.SendActivityAsync($"click here to sign in {url}");
    

    然后从用户那里取回一个数字代码作为消息,您可以使用他的回答中提到的 GetUserTokenAsync() 上交该消息。不过,卡片登录似乎更简洁一些,因为它不需要弹出外部登录窗口,并且 OAuthPrompt 对话框将令牌检索/登录包装为一个步骤。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-05
      相关资源
      最近更新 更多