question

RobTaylor-2829 avatar image
0 Votes"
RobTaylor-2829 asked RobTaylor-2829 commented

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

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://docs.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

azure-ad-adal-deprecation
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

ShwetaMathur avatar image
1 Vote"
ShwetaMathur answered RobTaylor-2829 commented

Hi @RobTaylor-2829

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.




img1.png (85.8 KiB)
img2.png (54.9 KiB)
img3.png (72.2 KiB)
· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

RobTaylor-2829 avatar image
0 Votes"
RobTaylor-2829 answered

@ShwetaMathur - 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



image.png (54.3 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.