Getting an id token from the Azure Bot Service when leveraging Bot Authentication in Microsoft Teams

The Azure Bot Service can be used together with the Bot Framework SDK to augment your applications with enhanced user experiences by introducing conversational AI.

Getting an id token from the Azure Bot Service when leveraging Bot Authentication in Microsoft Teams
Photo by ConvertKit / Unsplash

The Azure Bot Service can be used together with the Bot Framework SDK to augment your applications with enhanced user experiences by introducing conversational AI. One popular use case for this technology is building a Bot for Microsoft Teams, in this post we will focus on Bot Authentication and how we can acquire an id_token instead of the default which is to provide an access_token.

The Azure documentation does a good job explaining the authentication flow for bots in Microsoft Teams, I wont rehash this information here but I would suggest that you read through this documentation before continuing. This diagram from the official docs gives us insight into the steps executed during authentication.

The Azure Bot Service supports authenticating end-users, this allows our bot to call other services on behalf of our authenticated user.  Out of the box the bot service supports a number of identity providers, leveraging a provider pattern means that the service is extensible and new identity providers can be added over time. See the diagram below from the official docs which highlights the key components and interactions related to authentication.

A key point to understand here is the the Bot Framework handles acquiring, renewing and caching tokens on your behalf using sign-in cards and services which are based on OAuth and leverage a connection string provided by the developer.

As the typical authentication flows leveraged by bots are On-Behalf-Of and Auth Code the Bot service currently only acquires access_tokens, all other tokens returned by an Identity Provider will be ignored i.e. id_token.

Our scenario described below is based on the fact that  the Bot Framework supports the concept of Identity Provider Proxies, a proxy sits between the OAuth Identity provider and the Bot token service.

Using Bot Authentication to acquire an id_token

To demonstrate this scenario I have forked one of the official Microsoft Teams Authentication samples,  if you wish to test this in your own subscription please follow the getting started section of the Readme. For the rest of this post I will highlight where I deviated from these instructions to achieve the expected result.

  • Implement Identity Provider Proxy
  • Configure Bot Service to use our Proxy
  • Update Teams Bot to leverage id_token

Implement Identity Provider Proxy

For our proxy we need to implement two API's, in this case we will be wrapping the Azure Active Directory Authentication and Token Endpoints, and I will change the behavior of the Token endpoint to return our id_token. I based my nodejs implementation on the sample provided by the official documentation, for simplicity I included the required logic in the node back-end for my Teams Bot .

Authorize Endpoint

app.get("/api/oauth/authorize", (req, res) => {
    let response_type : string = req.query.response_type.toString();
    let client_id: string = req.query.client_id.toString();
    let state: string = req.query.state.toString();
    let redirect_uri: string = req.query.redirect_uri.toString();
    let scope: string = req.query.scope.toString();

    if (!state)
    {
        res.sendStatus(400, "Authorize request missing parameter 'state'");
    }

    if (!redirect_uri)
    {
        res.sendStatus(400, "Authorize request missing parameter 'redirect_uri'");
    }

    // Redirect call to AAD with all supplied params as is
    res.redirect(`https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${client_id}&response_type=${response_type}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`);
});

Token Endpoint

app.post("/api/oauth/token", (req, res) => {
    let authorizationCode  : string = req.body.code.toString();
    let grantType : string = req.body.grant_type.toString();
    let clientId : string = req.body.client_id.toString();
    let clientSecret : string = req.body.client_secret.toString();
    let redirectUri: string = req.body.redirect_uri.toString();

    // Request our tokens from AAD using supplied params 
    client.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', qs.stringify({
      client_id: `${clientId}`,
      client_secret: `${clientSecret}`,
      redirect_uri: `${redirectUri}`,
      grant_type: `${grantType}`,
      code: `${authorizationCode}`
    }),
    {
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    })
    .then((response) => {
        console.dir(response.data);
        // Return Id or Access Token as required
        res.json({
            //access_token : response.data.access_token,
            id_token : response.data.id_token,
            expires_in: response.data.expires_in
        });
    })
    .catch((error) => {
      logger.error(error);
      // relay the response
      if( error.response ){
        res.sendStatus(error.response.status);
      }
    });
});

Configure Bot Service to use our Proxy

Once we have our Proxy ready we need to open our Azure Bot resource in the Azure Portal -> Configuration, click Add OAuth Connection Settings Select the Generic OAuth 2 provider and complete the required fields:

  • Name - ProxyAAD
  • Client Id and Secret of the AAD Application
  • Authorization Url - https://[ID].ngrok.io/api/oauth/authorize
  • Token Url - https://[ID].ngrok.io/api/oauth/token
  • Refresh Url - https://[ID].ngrok.io/api/oauth/token
  • Scopes - openid ( Ensures an id_token is included in the tokens returned by AAD)

From the Provider Connection String blade we are able to test the supplied configuration by clicking the Test Connection button you will be prompted by Azure Active Directory to authenticate and should be redirected to the screen below with the returned token.

To confirm we have in actual fact received an id_token we can decode it with https://jwt.ms.

Update Teams Bot to leverage id_token

To demonstrate that our Teams bot receives an id token I made minor changes to the original sample to show the token claims.

    protected async showToken(stepContext: dialogs.WaterfallStepContext) {
        const tokenResponse: builder.TokenResponse = stepContext.result;
        if (tokenResponse) {
            const decodedToken = jwt.decode(tokenResponse.token, { complete: true });
            await stepContext.context.sendActivity(JSON.stringify(decodedToken));
            return await stepContext.next();
        } else {
            await stepContext.context.sendActivity("Please sign in so I can show you your token.");
            return await stepContext.replaceDialog(MAIN_WATERFALL_DIALOG);
        }
    }

For our simple test I set the AZUREAD_CONNECTIONNAME to ProxyAAD and launch my Bot App.

Once we authenticate leveraging our bot we have the option to Show our token, the returned token should be an id_token.

Caveats

An id_token should not be used for authorization, in this case leverage the default behavior of requesting an access_token. Depending on your scenario you may need both an access_token and an id_token, if this is the case you would need to define mulitple OAuth Connection strings.