Search code examples
c#oauthmicrosoft-graph-apiaccess-tokendynamics-365

Getting OpenID Connect / OAuth access token for calling MS Dynamics


I'm trying to wrap my head around the "modern" auth methods, and dealing with OAuth and access token for calling external services, in ASP.NET Core 5 MVC.

I have an app registration in Azure, which is set up OK - these are the API permissions for that app:

enter image description here

My goal is to call both MS Graph (several calls), and also MS Dynamics365, to gather some information. I've managed to set up authentication with OAuth (or is it OpenID Connect? I'm never quite sure how to tell these two apart) in my MVC app, like this (in Startup.cs):

/* in appsettings.json:
"MicrosoftGraph": {
    "Scopes": "user.read organization.read.all servicehealth.read.all servicemessage.read.all",
    "BaseUrl": "https://graph.microsoft.com/v1.0"
},
*/

public void ConfigureServices(IServiceCollection services)
{
    List<string> initialScopes = Configuration.GetValue<string>("MicrosoftGraph:Scopes")?.Split(' ').ToList();

    services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
        .AddInMemoryTokenCaches();
    
    // further service setups
}

It works fine - I'm prompted to log in, provide my credentials, and in my MVC app, I can check out the claims principals with its claims after logging in - so far, everything seems fine.

Next step is calling the downstream APIs. I studied some blog posts and tutorials and came up with this solution to fetch an access token, based on the name of a scope that I need for a given call. This is my code here (in a Razor page, used to show the data fetched from MS Graph):

public async Task OnGetAsync()
{
    List<string> scopes = new List<string>();
    scopes.Add("Organization.Read.All");

    // fetch the OAuth access token for calling the MS Graph API
    var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);

    HttpClient client = _factory.CreateClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    client.DefaultRequestHeaders.Add("Accept", "application/json");

    string graphUrl = "https://graph.microsoft.com/v1.0/subscribedSkus";
                       
    string responseJson = await client.GetStringAsync(graphUrl);
    
    // further processing and display of data fetched from MS Graph
}

For the MS Graph scopes, this works just fine - I get my access token, I can pass that to my HttpClient and the call to MS Graph succeeds and I get back the desired info.

The challenge starts when trying to use the same method for getting an access token to call MS Dynamics. I was assuming that I just specify the name of the API permission that is defined in the Azure AD registration - user_impersonation - like this:

public async Task OnGetAsync()
{
    List<string> scopes = new List<string>();
    scopes.Add("user_impersonation");

    var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
    // further code
}

But now I am getting nothing but errors - like this one:

An unhandled exception occurred while processing the request.
MsalUiRequiredException: AADSTS65001: The user or administrator has not consented to use the application with ID '257a582c-4461-40a4-95c3-2f257d2f8693' named 'BFH_Dyn365_Monitoring'. Send an interactive authorization request for this user and resource.

which is funny, because admin consent has been granted - so I'm not quite sure what the problem is .....

enter image description here

I then figured maybe I needed to add user_impersonation to the list of initial scopes (as defined in the appsettings.json and used in the Startup.ConfigureServices method) - but adding this results in another funny error:

An unhandled exception occurred while processing the request.
OpenIdConnectProtocolException: Message contains error: 'invalid_client', error_description: 'AADSTS650053: The application 'BFH_Dyn365_Monitoring' asked for scope 'user_impersonation' that doesn't exist on the resource '00000003-0000-0000-c000-000000000000'. Contact the app vendor.

Strange thing is - as you saw in the very first screenshot here - that scope IS present on the app registration - so I'm not totally sure why this exception is thrown....

Can anyone shed some light, maybe from experience of calling MS Dynamics using an OAuth token? Is this just fundamentally not possible, or am I missing a step or two somewhere?

Thanks! Marc


Solution

  • To get a token for user_impersonation for Dynamics (instead of Microsoft Graph), you should use the full scope value: "{your CRM URL}/user_impersonation".

    The full format for the values in the scopes parameter in GetAccessTokenForUserAsync is {resource}/{scope}, where {resource} is the ID or URI for the API you're trying to access, and {scope} is the delegated permission at that resource.

    When you omit the {resource} portion, the Microsoft Identity platform assumes you mean Microsoft Graph. Thus, "Organization.Read.All" is interpreted as "https://graph.microsoft.com/Organization.Read.All".

    When you attempt to request a token for "user_impersonation", the request fails because such a permission has not been granted for Microsoft Graph. In fact, such a permissions doesn't even exist (for Microsoft Graph), which explains the other error you see.