Dynamics Business Central Azure AD ADAL 未经授权

Dynamics Business Central Azure AD ADAL Unauthorized

我开发了一个基本的 express 应用程序来测试 NodeJS 中 Dynamics Business Central 和 ADAL 的身份验证。我收到以下 401 错误。身份验证在 Postman 中按预期工作,我能够在该上下文中调用 Dynamics REST 端点。在下面的 JavaScript 中,我在 Postman 中使用了相同的 AAD 租户、客户端 ID 和客户端密码,但我无法进行身份验证。

使用 https://jwt.io/ 比较了通过 Postman 和在 NodeJs 中给出的身份验证令牌,唯一的区别是负载中的 header 值和 uti

当我点击 getcompanies 路线时,出现以下错误。我在 post.

底部列出了我的节点包版本

错误 { error: { code: '401', message: 'Unauthorized' } }

源代码

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');
    }
  });
});

节点包

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

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.