3
votes

I have an Azure Function App (HTTP trigger function) that is protected by Azure AD authentication.

This is working fine - when I go to the URL of the function in my browser. I am first redirected to the login page where I can login using my Azure Active Directory login credentials.

However, I want the function to connect to the Dynamics 365 Web API to read some data using the identity of the logged in user. I've copied this example with regard to the code to connect to the Web API and configuring the app's permissions in Azure. My Dynamics 365 instance is in the same tenant as my Azure and the user I'm logging in with a user that has access to Dynamics 365.

The part I am struggling with is getting the access token to call Dynamics Web API that corresponds to the logged in user.

First attempt

I have then tried to use the token from the current logged in user, as per the "X-MS-TOKEN-AAD-ID-TOKEN" header. My code is as follows - it does not connect to the Dynamics Web API - I get a 401 - Unauthorized response:

[FunctionName("TestCrmFunction")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
    var crmUrl = "https://myorganisation.crm11.dynamics.com";

    // Get the token from the HTTP header of the request:
    var token = req.Headers
        .Where(x => x.Key == "X-MS-TOKEN-AAD-ID-TOKEN")
        .SelectMany(x => x.Value)
        .FirstOrDefault();

    using (var httpClient = new HttpClient())
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

        httpClient.BaseAddress = new Uri(crmUrl);

        // set the token here:
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        var response = await httpClient.GetAsync("api/data/v8.1/WhoAmI");

        var content = await response.Content.ReadAsStringAsync();

        if (response.IsSuccessStatusCode)
        {
            // I don't get here...
            return req.CreateResponse(HttpStatusCode.OK, content);
        }
        else
        {
            // ... I get here
            return req.CreateResponse(response.StatusCode, $"ERROR: {content}");
        }
    }

}

It seems that the "X-MS-TOKEN-AAD-ID-TOKEN" isn't being accepted.

Second attempt

I'm trying to get a new token for the user by creating a "UserAssertion" instance along with a ClientCredential. The UserAssertion bit I'm really not sure about and is probably wrong:

var tenantId = "<my organisation tenantID>";
var authority = $"https://login.microsoftonline.com/{tenantId}";
var authenticationContext = new AuthenticationContext(authority, false);

// create the client credential using the clientID and clientSecret:
var clientId = "<my applicationId>";
var clientSecret = "<key for my application>";
var clientCredential = new ClientCredential(clientId, clientSecret);

// create the user assertion using the access token from the authenticated user:
var accessToken = req.Headers
    .Where(x => x.Key == "X-MS-TOKEN-AAD-ACCESS-TOKEN")
    .SelectMany(x => x.Value)
    .FirstOrDefault();

var userAssertion = new UserAssertion(
    accessToken,
    "urn:ietf:params:oauth:grant-type:jwt-bearer",
    ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn)?.Value);

// get a new token - this step is failing
var crmUrl = "https://myorganisation.crm11.dynamics.com";
AuthenticationResult authenticationResult = 
    await authenticationContext.AcquireTokenAsync(crmUrl, clientCredential, userAssertion);

string token = authenticationResult.AccessToken;

The response I'm getting back is

Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException: AADSTS50027: Invalid JWT token. AADSTS50027: Invalid JWT token. Token format not valid.

Workaround - not a real solution

The only thing I've managed to get working is to hardcode a username/password of the user I'm logging in with:

var credentials = new UserPasswordCredential("<my username>", "<my password>");

AuthenticationResult authenticationResult = await 
authenticationContext.AcquireTokenAsync(crmUrl, clientId, credentials);

... but this kind of defeats the purpose - by hardcoding a username/password, I'm surely just circumventing the Azure Application Registration security altogether?

2
Hi, with your workaround, where do you get the clientId? I'm trying to do the same but it failed when I used System assisned managed identity as clientId.Khoait

2 Answers

0
votes

This is more of an alternative, but probably easier to achieve.

If you are able to identify the current user somehow, e.g. reading details from token. Then you can connect to CRM using a service account, but set the service object to impersonate the user.

Impersonate another user

Impersonate a user

To impersonate a user, set the CallerId property on an instance of OrganizationServiceProxy before calling the service’s Web methods.

// Retrieve the system user ID of the user to impersonate.
OrganizationServiceContext orgContext = new OrganizationServiceContext(_serviceProxy);
_userId = (from user in orgContext.CreateQuery<SystemUser>()
          where user.FullName == "Something from your token" //perhaps domain name?
          select user.SystemUserId.Value).FirstOrDefault();

// To impersonate another user, set the OrganizationServiceProxy.CallerId
// property to the ID of the other user.
_serviceProxy.CallerId = _userId;
0
votes

I've literally done the same thing by connecting an Azure Function App and providing it secure credentials to connect to dynamics 365, retrieve data, perform some data manipulations and then write the data back to Dynamics 365.

Please see my post below and if it helps your situation, hope you can mark this as answered.

Authenticate Azure Function App to connect to Dynamics 365 CRM online