Search code examples
azureoauth-2.0azure-active-directorybotframeworkmicrosoft-graph-api

Azure Active Directory v2 - Get Custom Scope Token


I am learning about generating a token for an OAuth service and it will be used in a chatbot. When I use the following code displayed below, I can get a default scope Graph Token successfully, and this token is valid for MS Graph API calls. Now, what I am trying to achieve is generating a custom scope token in a similar way in order to call an external service(Not MS Graph API). This token needs to have a custom scope. I tried to change the dictionary parameter "scope" to the name of my scope configured for a chatbot in Azure but it fails:

private async Task<string> GetGraphTokenAsync()
    {
        var dict = new Dictionary<string, string>();
        dict.Add("client_id", _graphTokenSettings.ClientId);
        dict.Add("client_secret", _graphTokenSettings.ClientSecret);
        dict.Add("scope", "https://graph.microsoft.com/.default");
        dict.Add("grant_type", "client_credentials");

        string gUrl = $"https://login.microsoftonline.com/{_graphTokenSettings.Tenant}/oauth2/v2.0/token";

        var client = new HttpClient();
        var req = new HttpRequestMessage(HttpMethod.Post, gUrl) { Content = new FormUrlEncodedContent(dict) };

        var httpResponseFromService = await client.SendAsync(req);
        httpResponseFromService.EnsureSuccessStatusCode();

        if (httpResponseFromService.Content is object
            && httpResponseFromService.Content.Headers.ContentType.MediaType == "application/json")
        {
            string stringFromservice = await httpResponseFromService.Content.ReadAsStringAsync();
            JObject tokenresponse = JsonConvert.DeserializeObject<JObject>(stringFromservice);
            string token = tokenresponse["access_token"].Value<string>();
            return token;
        }
        else
        {
            _logger.LogError($"Cannot get token for Microsoft Graph. httpResponseFromService.Content:{httpResponseFromService.Content}" );
            throw new Exception("Cannot get token for Microsoft Graph.");
        }
    }

The provider configuration in my Bot is the following, is it using as Service Provider: Azure Active Directory v2:

enter image description here

This is an example of a custom token generated with an OAuth tool (tenant id and other values changed to just illustrate the data, but all these values match and are correct when working with them), it is calling to the same url "login.microsoftonline.com" that I am trying to call to generate the custom scope token:

enter image description here

This generated custom scope token works. It has been configured at my Tenant Azure level as "api://botid-GUID/access_as_user" but I would like to generate it via http client as my code example. Would you know how can I get a token using this custom scope with a similar httpClient approach? It seems the scope parameter that I am sending ("api://botid-GUID/access_as_user") is not correct for client_credentials grant type call:

Default scope:

 dict.Add("client_id", _graphTokenSettings.ClientId);
        dict.Add("client_secret", _graphTokenSettings.ClientSecret);
        dict.Add("scope", "https://graph.microsoft.com/.default");
        dict.Add("grant_type", "client_credentials");

Replaced by:

 dict.Add("client_id", _graphTokenSettings.ClientId);
        dict.Add("client_secret", _graphTokenSettings.ClientSecret);
        dict.Add("scope", "api://botid-GUID/access_as_user");
        dict.Add("grant_type", "client_credentials");

Any help will be very appreciated.


Solution

  • I tried to reproduce the same in my environment and got below results:

    I have one Azure AD application where I created one custom scope by exposing the API like below:

    enter image description here

    I registered another application named ClientApp and added above custom scope by granting consent like below:

    enter image description here

    In my Azure Bot, I added one connection setting with Service Provider as Azure Active Directory v2 like below:

    enter image description here

    When I ran Test connection, I got the token successfully like below:

    enter image description here

    When I decoded the above token, I got claims with scope as below:

    enter image description here

    When you create custom scope by exposing an API, it comes under Delegated permissions that involves user interaction like below:

    enter image description here

    Note that, client credential flow only works with Application permissions that does not involve user interaction.

    You need to create App role instead of exposing the API in the application with different unique value access-as-user like below:

    enter image description here

    You can add above App role to your client application that comes under Application permissions and make sure to grant consent as below:

    enter image description here

    In addition to that, client credentials grant type supports scope that ends with only /.default while using v2 endpoint. Otherwise, it will throw exception like below:

    enter image description here

    To resolve the above error, you need to replace scope with /.default at end like below while generating token:

    POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
    client_id:appID
    grant_type:client_credentials
    client_secret:secret
    scope: api://87xxxa-6xex-4dxa-9xaf-b1dxxxx9819/.default
    

    Response: enter image description here

    When I decoded the above token, I got claims with roles as below:

    enter image description here

    Note that, decoded token contains Application permissions in roles claim whereas Delegated permissions in scp claim.

    In your scenario, if you want to use custom scope with client credentials grant type, you need to create App role with unique value that comes under Application permissions.

    Make sure to change scope with /.default at end.