microsoft / botbuilder-dotnet

Welcome to the Bot Framework SDK for .NET repository, which is the home for the libraries and packages that enable developers to build sophisticated bot applications using .NET.
https://github.com/Microsoft/botframework
MIT License
871 stars 478 forks source link

OAuthPrompt in v4.10.3 + Generic Oauth 2 + IdentityServer = No prompt is shown #4927

Closed dejancg closed 3 years ago

dejancg commented 3 years ago

OAuthPrompt in Microsoft.Bot.Builder.Dialogs v4.10.3 seems to be broken when using the connection name of "Generic Oauth 2" service provider - it is not displayed in Teams dialog with the bot, nor in WebChat.

To Reproduce

  1. Set up an IdentityServer instance locally, which needs to have the client configured as follows:
    • ClientId: your_client_id
    • ClientSecret: your_client_secret
    • RedirectUris: ["https://token.botframework.com/.auth/web/redirect"]
    • RequireConsent: false
    • AllowOfflineAccess: true,
    • AllowedScopes: ["openid", "profile"],
    • AllowedGrantTypes: ["authorization_code"],
    • RequirePkce: false, <-- necessary so Bot services can use it, as Bot services don't seem to support PKCE
    • RefreshTokenUsage: TokenUsage.ReUse <-- the Bot services seem unable to handle the one-time refresh tokens
  2. Configure the Generic Oauth 2 service provider in Bot Channels registration so it points to the IdentityServer instance (you can use ngrok to host the IdentityServer instance) like this:
    • Client id: your_client_id,
    • Client secret: your_client_secret,
    • Authorization URL: https://<your identity server address>/connect/authorize,
    • Token URL: https://<your identity server address>/connect/token,
    • Refresh URL: https://<your identity server address>/connect/token,
    • Scopes: openid profile
  3. Start from the Teams Auth sample
  4. Edit appsettings.json to enter the valid credentials and use the connection name of AADv2 service provider
  5. Start the bot, type "hi" or whatever in Teams conversation with the bot and it will display a sign-in card.
  6. Stop the bot, go back to appsettings.json and change the connection name to have the name of the configured Generic Oauth 2 service provider connection name
  7. Start the bot again, type "hi" or whatever in Teams convo with the bot and it won't display anything.

Screenshots

The configuration of Generic OAuth 2 service provider connection: Generic Oauth 2

Additional context

This is the output from VS debugging:

Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Route matched with {action = "Post", controller = "Bot"}. Executing controller action with signature System.Threading.Tasks.Task PostAsync() on controller Microsoft.BotBuilderSamples.BotController (TeamsAuth).
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: Received an incoming activity.  ActivityId: 1605218380039
Microsoft.BotBuilderSamples.DialogBot: Information: Running dialog with Message Activity.
TeamsAuth Information: 0 : 'hi' ==> beginDialog      ==> MainDialog 
TeamsAuth Information: 0 : 'hi' ==> beginDialog      ==> WaterfallDialog 
TeamsAuth Information: 0 : 'hi' ==> beginDialog      ==> OAuthPrompt 
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: GetTokenAsync: Acquired token using ADAL in 0.
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in Microsoft.Rest.ClientRuntime.dll
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in System.Private.CoreLib.dll
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: GetTokenAsync: Acquired token using ADAL in 0.
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: Sending activity.  ReplyToId: 1605218380039
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: GetTokenAsync: Acquired token using ADAL in 0.
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executed action Microsoft.BotBuilderSamples.BotController.PostAsync (TeamsAuth) in 746.9577ms
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executed endpoint 'Microsoft.BotBuilderSamples.BotController.PostAsync (TeamsAuth)'
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 762.0714ms 200 

Note that the sign in via this connection name works fine when used with messaging extensions:

new MessagingExtensionResult
{
    Type = "auth",
    SuggestedActions = new MessagingExtensionSuggestedAction
    {
        Actions = new List<CardAction>
        {
            new CardAction
            {
                Type = ActionTypes.OpenUrl,
                Value = signInAddress,
                Title =  title,
            }
        }
    }
};

Also, if I sign in through the messaging extension first, the OAuthPrompt will correctly retrieve the access token for either requested connection name.

Also.... I tried with an older 46.teams-auth sample which used v4.6.2 and it also worked fine. Pls fix 🥇

jwiley84 commented 3 years ago

Does this also occur if you use "Oauth 2 Generic Provider"? image

jwiley84 commented 3 years ago

Also, what error message do you get when it doesn't show up?

dejancg commented 3 years ago

Hello @jwiley84 , I don't know. We don't use that provider. It has too many fields to configure (which also seem to be undocumented) and we don't want to provide support to our customers regarding something as unintuitive as that.

Note that "Generic Oauth 2" works fine with an older Microsoft.Bot.Builder.Dialogs.

dejancg commented 3 years ago

@jwiley84 no error message. Just doesn't show up (you can try with the sample I mentioned, it's not hard to reproduce). In our application, however, BeginDialogAsync method of OAuthPrompt throws an error with HTTP status code 404.

dejancg commented 3 years ago

Any update on this @jwiley84 ? Update: The prompt doesn't work with web chat channel either.

jwiley84 commented 3 years ago

Hi @dejancg

I'm able to partially reproduce this: image

that being said, I say partial because I'm not getting the card because I believe my token request is malformed somehow. I am running my bot via command-line and I'm getting the following error: image

I suspect that this is because the teams auth sample is expecting AAD v2/MSGraph. Are you getting the same error?

dejancg commented 3 years ago

@jwiley84 I haven't observed this error. Also, I see your AdapterWithErrorHandler is catching an exception. There was no such exception on my side. Instead, the bot would've just entered an invalid state where any subsequent request from the same user would just fail.

I don't think this is because the teams auth sample is expecting AAD v2 provider. The generic Oauth2 provider works fine when I login from a messaging or action messaging extension. I suspect this has something to do with OAuthClient used internally by OAuthPrompt. Try to use BotFrameworkAdapter.CreateOAuthClient from an older branch (say, 4.6) and you will see that the sign in card will be displayed properly.

jwiley84 commented 3 years ago

In that case, I am unable to repro your issue. This is using 4.10.3. The signin card you see at the top of my screen shot IS the sign in card from the Generic Auth 2 setup, using Twitch.tv as my generic provider. I was able to sign in, though I ran into my blocker after signing in.

I dislike being the "have you turned it off and on again" person, but possibly something is wrong with the teams app itself? Have you tried creating a new bot channels registration and re-uploading the app to teams?

dejancg commented 3 years ago

Bot Framework reminds me of Windows 95, where if you would eject the CD while it's playing audio, you would get a BSOD.

You were probably using the emulator, since you are getting these messages? I will try with the emulator too and see if there is any helpful information. I don't see what could possibly be wrong with the bot channels registration, since the entire sign in process works fine when using any messaging extension. Please take a look at the following code:

TokenStatus[] tokenStatusList = null;
try
{
    // Check if the user has a token stored already in the bot framework service.
    tokenStatusList = await botAdapter
        .GetTokenStatusAsync(turnContext, turnContext.Activity.From.Id, oauthConnectionName, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
    throw;
}

// Get the current relevant connection
var tokenStatus = tokenStatusList?.FirstOrDefault(ts => ts.ConnectionName == oauthConnectionName);

// If token is found, fetch the token for the required resources
if (tokenStatus?.HasToken != null && tokenStatus?.HasToken == true)
{
    var tokenResponseDictionary = await botAdapter.GetAadTokensAsync(turnContext,
                                                                     oauthConnectionName,
                                                                     new string[] { "myCustomAudience" },
                                                                     turnContext.Activity.From.Id,
                                                                     cancellationToken).ConfigureAwait(false);

    if (tokenResponseDictionary != null)
    {
        accessToken = tokenResponseDictionary["myCustomAudience"].Token;
    }
}

// If token was not present in the bot framework service, check if we are in the middle of the sign in flow. 
// This is done by checking if the request from Teams to the bot service contains a magic code in the "turnContext.Activity.Value.state" property.
// If we are in the middle of the auth flow and magic code is found, fetch the token using the magic code.
dynamic res = turnContext.Activity.Value;
if (string.IsNullOrEmpty(accessToken) && res != null)
{
    var state = res.state;
    if (state != null)
    {
        string stateString = state;
        var tokenResponse = await botAdapter.GetUserTokenAsync(turnContext, oauthConnectionName, stateString, cancellationToken).ConfigureAwait(false);
        if (tokenResponse != null)
        {
            accessToken = tokenResponse.Token;
        }
    }
}

//4. If access token is still empty, this means the user has not signed in to the bot. Send the user a sign-in action.
if (string.IsNullOrEmpty(accessToken))
{
    var oAuthSignInLink = await botAdapter.GetOauthSignInLinkAsync(turnContext, oauthConnectionName, cancellationToken).ConfigureAwait(false);
    return (false, oAuthSignInLink);
}

return (true, accessToken);

This is a function which checks whether users are signed in, and if so, retrieves the access token. Else, the function would return the sign-in link. The sign-in link is afterwards displayed to users by using the following code:

var composeExtensionResult = new MessagingExtensionResult
{
    Type = "auth",
    SuggestedActions = new MessagingExtensionSuggestedAction
    {
        Actions = new List<CardAction>
        {
            new CardAction
            {
                Type = ActionTypes.OpenUrl,
                Value = signInAddress, // the address returned from the previous method
                Title =  title,
            }
        }
    }
};

and returned as either MessagingExtensionResponse or MessagingExtensionActionResponse, depending on the messaging extension type.

I hope you agree that, if the above procedure works fine, this issue can't be the bot channels registration's fault. Additionally, regardless of Teams app being used or not, this issue will persist even if used from a Web Chat channel. So it can't be the Teams app's fault either.

What bothers me is that you are not able to reproduce it. Twitch or any other Oauth2 provider, it shouldn't make any difference.

jwiley84 commented 3 years ago

No. It would have been rather silly of me to try to repro a teams issue on emulator as they behave differently. That being said: I used the Teams Auth Sample (https://github.com/microsoft/BotBuilder-Samples/tree/main/samples/csharp_dotnetcore/46.teams-auth) and the Teams channel. Let me attempt a repro using your code and let you know what I get.

dejancg commented 3 years ago

Here is the output from VS, if it can be of any help:

Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Route matched with {action = "Post", controller = "Bot"}. Executing controller action with signature System.Threading.Tasks.Task PostAsync() on controller Microsoft.BotBuilderSamples.BotController (TeamsAuth).
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: Received an incoming activity.  ActivityId: 1605218380039
Microsoft.BotBuilderSamples.DialogBot: Information: Running dialog with Message Activity.
TeamsAuth Information: 0 : 'hi' ==> beginDialog      ==> MainDialog 
TeamsAuth Information: 0 : 'hi' ==> beginDialog      ==> WaterfallDialog 
TeamsAuth Information: 0 : 'hi' ==> beginDialog      ==> OAuthPrompt 
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: GetTokenAsync: Acquired token using ADAL in 0.
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in Microsoft.Rest.ClientRuntime.dll
Exception thrown: 'Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException' in System.Private.CoreLib.dll
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: GetTokenAsync: Acquired token using ADAL in 0.
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: Sending activity.  ReplyToId: 1605218380039
Microsoft.Bot.Builder.Integration.AspNet.Core.BotFrameworkHttpAdapter: Information: GetTokenAsync: Acquired token using ADAL in 0.
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker: Information: Executed action Microsoft.BotBuilderSamples.BotController.PostAsync (TeamsAuth) in 746.9577ms
Microsoft.AspNetCore.Routing.EndpointMiddleware: Information: Executed endpoint 'Microsoft.BotBuilderSamples.BotController.PostAsync (TeamsAuth)'
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished in 762.0714ms 200 

It seems it finishes the request with status code 200. Is there a way to inspect the response?

dejancg commented 3 years ago

@jwiley84 I have new information. OAuthPrompt works with 4.10 and Generic Oauth2 provider using Github app info. This means I have to update the repro steps - you need to set up an instance of Identity Server and use that info in Generic Oauth2 provider to reproduce this issue.

However, here is an interesting thing - I switched to "4.8" branch of BotBuilder-Samples and OAuthPrompt works fine there. So to work around this, I will downgrade my BotBuilder libraries to 4.8 as well, until you guys work this thing out.

editing repro steps now

dejancg commented 3 years ago

@jwiley84 it seems I made a mistake when configuring the Generic Oauth2 provider after all. BotBuilder 4.10 is probably using the Token Exchange URL which wasn't used by the previous BotBuilder versions. The problem was that I wrongfully set the value of Token Exchange URL to the token endpoint URL, where it had to be left blank. When I cleared the value, since it's an optional field, the OAuthPrompt started to show, even with 4.10. Please accept my apologies for wasting your time on this issue.