Upgrading from ADAL to MSAL - using dynamics-web-api NodeJS

Rob Taylor 21 Reputation points
2022-01-26T23:55:11.087+00:00

I have a Nodejs app that uses adal-node and dynamics-web-api (https://www.npmjs.com/package/dynamics-web-api) to query our Dynamics 365 instance. I'm trying to migrate from ADAL to MSAL, roughly following these guidelines: https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-node-migration

Old code using ADAL does this to obtain an access token - it's been working fine:

var DynamicsWebApi = require('dynamics-web-api');
var clientId = '[My Azure registered apps client ID]';
var AuthenticationContext = require('adal-node').AuthenticationContext;

//OAuth Token Endpoint
var authorityUrl = 'https://login.microsoftonline.com/[Azure tenant ID]/oauth2/token';

//CRM Organization URL
var resource = 'https://[my domain].crm.dynamics.com/';
var username = '[DYNAMICS_USERNAME]';
var password = '[DYNAMICS_PASSWORD]';
var adalContext = new AuthenticationContext(authorityUrl);
var accessToken='';

//add a callback as a parameter for your function
function acquireToken(dynamicsWebApiCallback){
    //a callback for adal-node
    function adalCallback(error, token) {
        if (!error){
            //call DynamicsWebApi callback only when a token has been retrieved
            accessToken=token.accessToken;
            dynamicsWebApiCallback(accessToken);

            // pass back the access token
            callback(null,accessToken);
        }
        else{
            console.log('Token has not been retrieved. Error: ' + error.stack);
            callback(error,null);
        }
    }
    //call a necessary function in adal-node object to get a token
    adalContext.acquireTokenWithUsernamePassword(resource, username, password, clientId, adalCallback);
}

var dynamicsWebApi = new DynamicsWebApi({
    webApiUrl: 'https://[my domain].api.crm.dynamics.com/api/data/v9.0/',
    onTokenRefresh: acquireToken
});

//call any function
dynamicsWebApi.executeUnboundFunction("WhoAmI").then(function (response) {
    callback(null,accessToken);
}).catch(function(error){
    callback(error,null);
});

/*************************************************************************************************/

Below is new code using MSAL. The call to acquireTokenByUsernamePassword() correctly returns a token, but when I attempt to use the accessToken in subsequent calls to query Dynamics via the DynamicsWebApi API, I get 401 Unauthorize errors.

  • I've added the API permissions for Dynamics CRM / Dataverse to the app in Azure - you can see that in the scopes
  • The account I'm using has the necessary privileges - same account works fine in the ADAL solution above
  • not sure what I'm missing var DynamicsWebApi = require('dynamics-web-api'); var msal = require("@azure/msal-node"); var accessTemp = ''; const clientConfig = { auth: { clientId: '[My Azure registered apps client ID]', authority: 'https://login.microsoftonline.com/[tenant ID]', } }; const pca = new msal.PublicClientApplication(clientConfig); const usernamePasswordRequest = { scopes: [ 'https://admin.services.crm.dynamics.com/user_impersonation', 'https://graph.microsoft.com/User.Read'], username: '[DYNAMICS_USERNAME]', password: '[DYNAMICS_PASSWORD]', }; //add a callback as a parameter for your function function acquireToken(dynamicsWebApiCallback){ pca.acquireTokenByUsernamePassword(usernamePasswordRequest) .then(response => { accessToken = response.accessToken; dynamicsWebApiCallback(accessToken); callback(null,accessToken); }) .catch(error => { console.error('Token has not been retrieved. Error: ' + error.stack); callback(error,null); }); } var dynamicsWebApi = new DynamicsWebApi({ webApiUrl: 'https://[my domain].api.crm.dynamics.com/api/data/v9.0/', onTokenRefresh: acquireToken }); //call any function dynamicsWebApi.executeUnboundFunction("WhoAmI").then(function (response) { console.log('executed whoami:', response); callback(null,accessToken); }).catch(function(error){ callback(error,null); }); /*************************************************************************************************/ 401 Unauthorized error details: "status_code": 200, "content_type": "application/json", "parsed": { "message": "Unexpected Error", "status": 401, "statusMessage": "Unauthorized", "headers": { "allow": "OPTIONS,GET,HEAD,POST", "x-ms-service-request-id": "2de8402d-b5da-493d-b190-xxxxxxxxxxxx, f818649e-cce3-49d8-bd91-xxxxxxxxxxxx", "set-cookie": [ "ARRAffinity=838ff9bc421a5390cb60466a3e7023695ec37110a091f2b71999xxxxxxxxxxxx; domain=xxxxxxxxxxx.api.crm.dynamics.com; path=/; secure; HttpOnly" ], "www-authenticate": "Bearer authorization_uri=https://login.microsoftonline.com/[xxxxxxxxxxx]/oauth2/authorize, resource_id=https://xxxxxxxx.api.crm.dynamics.com/", "strict-transport-security": "max-age=31536000; includeSubDomains", "req_id": "f818649e-cce3-49d8-bd91-xxxxxxxxxxxx", "authactivityid": "d72a04dd-0f6c-48b2-9747-xxxxxxxxxxxx", "nativewebsession-version": "2", "x-source": "170824215124150113103417514167243873422349142230147229211401501981611161241xxxxxxxxxxxx, 88422011721969210810713227100195981721461771441015125146249147142152211xxxxxxxxxxxx", "public": "OPTIONS,GET,HEAD,POST", "date": "Wed, 26 Jan 2022 23:37:07 GMT", "content-length": "0" }

Any assistance would be appreciated!

Thanks, Rob Taylor

Microsoft Entra ID
Microsoft Entra ID
A Microsoft Entra identity service that provides identity management and access control capabilities. Replaces Azure Active Directory.
19,389 questions
0 comments No comments
{count} votes

Accepted answer
  1. Shweta Mathur 27,141 Reputation points Microsoft Employee
    2022-01-27T13:18:20.853+00:00

    Hi @Rob Taylor

    Thanks for reaching out.

    The scope defined in the MSAL application is not matching the audience of the Dynamics CRM/Data verse.

    The audience “aud” claim in a Access token is meant to refer to the Resource Servers that should accept the token. When a recipient is validating the JWT , it validate that the token was intended to be used for its purposes, it MUST determine what value in aud and the token should only validate if the recipient's declared ID or URL is present in the aud claim.

    You can validate the access token you are getting through using jwt.ms.

    I have replicated the scenario in my lab , and I am able to get access token using ResourceOwnerPasswordCredential (Username Password) flow with right audience and able to call Dynamics CRM successfully.

    168997-img1.png

    169064-img2.png

    With valid scope and audience in the access token, I can call the Dynamic CRM API successfully.

    169074-img3.png

    In your scenario the scope you mentioned in the application "https://admin.services.crm.dynamics.com/" is not matching with the audience of Dynamic CRM API and not allowing your application to access the API.

    Please update the scope of your MSAL application with the "https://[my domain].crm.dynamics.com/" as you mentioned in ADAL application
    var resource = 'https://[my domain].crm.dynamics.com/' for successful API call.

    Thanks,
    Shweta

    ---------------------------------------

    Please remember to "Accept Answer" and "Up-Vote if answer helped you.

    1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. Rob Taylor 21 Reputation points
    2022-01-28T22:25:13.797+00:00

    @Shweta Mathur - thanks so much. I got tripped up trying to match up scopes with the permissions assigned to the Azure App.
    I had added the user impersonation for Dataverse permission, and assumed that I had to use the "https://admin.services.crm.dynamics.com/user_impersonation" value for that permission in the scopes, when in fact I needed to add the https://[my domain]/crm.dynamics.com string, as you stated.

    169524-image.png

    0 comments No comments