Search code examples
botframeworkmicrosoft-teams

In the bot framework OAuth Flow with Teams, what is the correct way to consent to changed permissions?


I've implemented the OAuth flow in a Teams bot similar to the sample, and I'm able to get a user token and make calls against the graph, but I'm running into an issue with changing permissions. Here's what I did:

  • Created a bot and gave it mail.read
  • Add the bot for a user and trigger the OAuth dialog. User is prompted for consent, and signed in.
  • Successfully read mail
  • In AAD, added the Mail.ReadWrite and Mail.Send permissions to the app
  • Next time the OAuthPrompt was triggered, token was returned immediately. No prompt for consent.
  • Got an access denied exception when trying to write or send mail

So the issue is that changing permissions on the app does not automatically trigger the consent flow again in the OAuthPrompt. I also tried these troubleshooting steps:

  • I manually signed the user out (botAdapter.SignOutUserAsync) and signed back in, but was not prompted for consent again.
  • Same as above, but signed out of Teams as well. Launched the web client in a new incognito window, and still no prompt for consent.
  • Uninstalled and re-installed the bot on the user's account. No change.
  • Tried the above after waiting a few hours, but still no change.

The only way I found to trigger the consent flow again was to have the user go to https://account.activedirectory.windowsazure.com/r#/applications and delete the consent from there. Even then I had to call botAdapter.SignOutUserAsync (since the bot service still returned a token with the old permissions). Once I did that, and triggered the OAuth prompt again, I was able to get the consent flow with the new permissions to trigger.

So my question is, is there a better way to handle this? If a new version of the bot requires new permissions, shouldn't the OAuthPrompt from Microsoft.Bot.Builder.Dialogs and the bot service handle re-prompting for consent?


Solution

  • Short answer

    Bot Framework's auth provider for AAD v1 isn't flexible enough to support adding scopes.

    You can switch to the AAD v2 provider and specify your new scopes in the OAuth Connection Settings. Then, if you force the user to sign in again (after SignOutUserAsync), you will get a consent screen including the new scopes. To set up the AAD v2 provider, see the Bot Framework docs on adding authentication to a bot, which has steps for both the AAD v1 and AAD v2 providers.

    Long answer

    The key to this behavior is how the AAD v1 and AAD v2 endpoints handle permissions and consent differently.

    The problem with v1

    In v1, the permissions (such as Mail.Send) are pre-registered in the AAD app registration. During sign-in, AAD checks if the user has already consented to any scopes for the app.

    • If yes, AAD skips the consent screen and eventually provides an access token for the scopes that had already been consented.
    • If no, AAD shows the consent screen for all registered scopes and eventually provides an access token for all of those scopes.

    This explains the behavior you're seeing. After you tell Bot Framework to forget the current access token (via SignOutUserAsync) and force the user to login again, AAD sees that the user has consented previously, so it skips the consent screen and gives you a new token with the old scopes.

    Then how do you add permissions for users who have already consented, without forcing the user to delete consent for the app? AAD's login endpoint has an optional prompt parameter that you can set to prompt=consent. This will force AAD to show the consent screen as if the user had not previously consented, and it will contain all registered permissions. So if you try to use an access token and get a 403 Forbidden error (or an equivalent exception), you can take the user through the login flow using prompt=consent.

    Unfortunately, with Azure Bot Service's AAD v1 provider, you don't have enough control over the login URL to dynamically set the prompt parameter, so there isn't an easy way to achieve this.

    Enter v2

    But there's hope! The AAD v2 endpoints have a much more flexible way of adding scopes incrementally. In v2, for delegated permissions, the permissions do not have to be pre-registered in the AAD app registration. Instead, you specify the scopes in the scope parameter of the login URL. During sign-in, AAD checks if the user has consented to the scopes you specified in the URL.

    • If yes, AAD skips the consent screen and eventually provides an access token for the scopes you specified.
    • If no, AAD shows the consent screen for all the scopes you specified and eventually provides an access token for all of those scopes.

    Either way, you end up with an access token containing all the scopes you specified in the URL.

    Bot Framework sets the scope parameter using the scopes you specify in the OAuth Connection Settings. So if you add a new scope there, then the next time the user signs in, they will get a consent screen with the new permissions, and you will get an access token with the new scopes. (Note: To re-trigger the sign-in, you will still have to sign the user out using SignOutUserAsync. Otherwise, Bot Framework will continue giving you the access token it already has, instead of performing a new sign-in flow with AAD.)