greg
greg

Reputation: 1204

Dynamics Business Central Azure AD ADAL Unauthorized

I developed a bare bones express app to test auth with Dynamics Business Central and ADAL in NodeJS. I'm getting the following 401 error. The authentication works as expected in Postman and I'm able to call the Dynamics REST endpoint in that context. In the JavaScript below I am using the same AAD tenant, client id, and client secret in Postman, but I'm not able to authenticate.

Compared the auth tokens given over Postman and in NodeJs using https://jwt.io/ and the only difference is the header values and the uti in the payload.

When I hit me getcompanies route I get the following error. I've listed my node package versions at the bottom of the post.

Error { error: { code: '401', message: 'Unauthorized' } }

Source code

var AuthenticationContext = require('adal-node').AuthenticationContext;
var crypto = require('crypto');
var express = require('express');
var request = require('request');

require('dotenv').config()
var clientId = process.env.CLIENT_ID;
var clientSecret = process.env.CLIENT_SECRET;

var authorityHostUrl = 'https://login.windows.net';
var azureAdTenant = 'grdegr.onmicrosoft.com';

var dynBusinessCentralCommonEndpoint = 'https://api.businesscentral.dynamics.com/v1.0/' + azureAdTenant + '/api/beta';

var bcRedirectUri = 'http://localhost:1337/getbctoken';

var dynBusinessCentralAuthUrl = authorityHostUrl + '/' +
                        azureAdTenant +
                        '/oauth2/authorize?response_type=code&client_id=' +
                        clientId +
                        '&redirect_uri=' +
                        bcRedirectUri +
                        '&state=<state>&resource=' +
                        'https://api.businesscentral.dynamics.com';

var app = express();
var port = 1337;
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

app.get('/bcauth', function(req, res) {
  crypto.randomBytes(48, function(ex, buf) {
    var bcToken = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-');
    res.cookie('bcauthstate', bcToken);
    var dynBusinessCentralAuthUrlauthorizationUrl = dynBusinessCentralAuthUrl.replace('<state>', bcToken);

    console.log('redirecting to auth url: ' + dynBusinessCentralAuthUrlauthorizationUrl);
    res.redirect(dynBusinessCentralAuthUrlauthorizationUrl);
  });
});

var bcAccessToken = '';
app.get('/getbctoken', function(req, res) {

  var authorityUrl = authorityHostUrl + '/' + azureAdTenant;
  var authenticationContext = new AuthenticationContext(authorityUrl);

  console.log('getting bc auth context');
  authenticationContext.acquireTokenWithAuthorizationCode(
    req.query.code,
    bcRedirectUri,
    'https://api.businesscentral.dynamics.com/',
    clientId,
    clientSecret,
    function(err, response) {
      var message = '';
      if (err) {
        message = 'error: ' + err.message + '\n';
        return res.send(message)
      }

      bcAccessToken = response.accessToken;
      console.log('bc token\n' + bcAccessToken);

      res.send('bc access token updated');
    }
  );
});       

app.get('/getcompanies', (req, res) => {

  var body = '';
  var options = {
    url: 'https://api.businesscentral.dynamics.com/v1.0/grdegr.onmicrosoft.com/api/beta/companies',
    method: 'GET',
    headers: {
      Authorization: 'Bearer ' + bcAccessToken
    },
    json: JSON.stringify(body)
  };

  request(options, (err, response, body) => {
    res.send(response || err);

    if (response) {
      console.log(body);
    }
    else {
      console.log('response is null');
    }
  });
});

Node Packages

"devDependencies": {
    "adal-node": "^0.1.28",
    "request": "^2.87.0",
    "webpack": "^4.12.0",
    "webpack-cli": "^3.0.8"
  },
  "dependencies": {
    "dotenv": "^6.1.0"
  }

Upvotes: 0

Views: 649

Answers (1)

Philippe Signoret
Philippe Signoret

Reputation: 14356

Some services are very strict when checking the aud (audience) value of an access token. Dynamics 365 Business Central expects the access token audience to be exactly https://api.businesscentral.dynamics.com. In your code, you are asking for, and getting an access token for https://api.businesscentral.dynamics.com/. That trailing slash at the end is what is making Dynamics 365 reject your access token invalid.

Change the token request to:

authenticationContext.acquireTokenWithAuthorizationCode(
  req.query.code,
  bcRedirectUri,
  'https://api.businesscentral.dynamics.com', // <-- No trailing slash!
  clientId,
  clientSecret,
  // ...

...and it should work.

However, there are two important things to note in your sample:

  1. The pattern you are following is a bit strange, though it may be because you're in the early stages of development, or because it was just a minimal repro example for this question. You should not store an access token that way, because the next person who calls /getcompanies will be able to do so calling on behalf of the user who originally signed in, instead of signing in themselves. If you are looking to have users sign in with Azure AD and as part of that, call Dynamics 365 on behalf of the signed-in user, I suggest looking at passport-azure-ad.
  2. Especially if you plan to have a system-wide account or access token, be very careful returning the original response to the end user. This is true even when developing, since it's very easy to overlook something like that when moving to production, and exposing what could be a very privileged access token to an unauthorized user.

Upvotes: 1

Related Questions