如何从Web App(节点/ Express)查询本地Dynamics CRM

An0*_*d3r 20 microsoft-dynamics adfs dynamics-crm node.js azure-active-directory

一直在用几个墙撞到我的头上,所以希望一些CRM/Dynamics专家可以帮我一臂之力!

我正在尝试以编程方式从我们的Dynamics CRM实例中获取数据,在Node支持的Express应用程序中使用一组管理凭据.此Express应用程序托管在托管CRM的网络之外的单独服务器上.然后,应用程序将请求,处理并将CRM数据提供给任何已访问的用户(由应用程序中的角色/权限控制),这意味着最终用户只需登录Express应用程序,而无需登录通过ADFS,以便应用程序访问CRM实例.

我们的CRM设置是一个配置为面向互联网(IFD)的内部部署服务器.这使用Active Directory联合身份验证服务.我们的Web应用程序代理服务器在网络的外围运行联合服务,与内部网络上的ADFS服务器进行通信.ADFS对从网络外部(从Internet)连接到内部AD的用户进行身份验证.经过身份验证后,代理允许用户连接到CRM.

我们的本地活动目录与Azure AD同步,因为我们有混合部署.任何O365服务(在线交换,共享点等)都在后台使用Azure AD.我们同步Active目录,因此我们只需要在一个地方管理用户.

CRM有一个端点,例如https://my.crm.endpoint,我在Azure门户中注册了一个应用程序(称为CRM App),主页设置为CRM端点https://my.crm.endpoint.

问题是否将应用程序的主页设置为https://my.crm.endpoint足以将其"链接"到我们的内部CRM实例?

我编写了一个脚本(crm.js),它使用它的应用程序ID 成功请求在Azure门户中注册的CRM应用程序的访问令牌.

示例令牌

eyJ0dWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzE5ZTk1...
Run Code Online (Sandbox Code Playgroud)

然后,我使用持票令牌尝试通过通常的端点从Dynamics获取一些联系人:https://my.crm.endpoint/api/data/v8.2/contacts?$ select = fullname,contactid

这失败了,我收到一条401 Unauthorised错误消息.

问题任何人都可以建议问题是什么?和/或提供有关如何连接Web应用程序(在我的案例中为Express)的详细信息,以便对使用ADFS的内部部署服务器(IFD)上运行的Dynamics CRM进行身份验证请求?

crm.js

let util = require('util');
let request = require("request");

let test = {
    username: '<my.email@address.com>',
    password: '<my_password>',
    app_id: '<app_id>',
    secret: '<secret>',
    authenticate_url: 'https://login.microsoftonline.com/<tenant_id>/oauth2/token',
    crm_url: 'https://<my.crm.endpoint>'
};
function CRM() { }

CRM.prototype.authenticate = function () {
    return new Promise((resolve, reject) => {
        let options = {
            method: 'POST',
            url: test.authenticate_url,
            formData: {
                grant_type: 'client_credentials',
                client_id: test.app_id,         // application id
                client_secret: test.secret,     // secret
                username: test.username,        // on premise windows login (admin)
                password: test.password,        // password
                resource: test.app_id           // application id
            }
        };

        // ALWAYS RETURNS AN ACCESS_TOKEN
        request(options, function (error, response, body) {
            console.log('AUTHENTICATE RESPONSE', body);
            resolve(body);
        });
    })
};

CRM.prototype.getContacts = function (token) {
    return new Promise((resolve, reject) => {

        let options = {
            method: 'GET',
            url: `${test.crm_url}/api/data/v8.2/contacts?$select=fullname,contactid`,
            headers: {
                'Authorization': `Bearer ${token}`,
                'Accept': 'application/json',
                'OData-MaxVersion': 4.0,
                'OData-Version': 4.0,
                'Content-Type': 'application/json; charset=utf-8'
            }
        };

        request(options, (error, response, body) => {
            console.log('getContacts', util.inspect(error), util.inspect(body));
            resolve(body);
        });

    });
};

let API = new CRM();    // instantiate the CRM object

API.authenticate()      // call authenticate function
    .then(response => {
        if (response) {

            let json = JSON.parse(response);
            let token = json.access_token;

            console.log('TOKEN', token);

            API.getContacts('token')
            .then(contacts => {
                // DO SOMETHING WITH THE CONTACTS
                console.log('CONTACTS', contacts);
            })
        }
    });


module.exports = CRM;
Run Code Online (Sandbox Code Playgroud)

错误响应

HTTP Error 401 - Unauthorized: Access is denied
Run Code Online (Sandbox Code Playgroud)

附加信息

我目前的解决方案是基于这些文档......

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service

UPDATE

根据@ andresm53的评论,我认为我确实需要直接对ADFS进行身份验证.我发现这篇博文描述了在ADFS中生成可与OAuth一起使用的共享密钥.

"使用这种形式的客户端身份验证,您可以将客户端标识符(作为client_id)和客户端密钥(作为client_secret)发送到STS端点.以下是此类HTTP POST的示例(使用客户端凭据授予,仅添加了换行符)为了便于阅读):"

resource=https%3a%2f%2fmy.crm.endpoint
&client_id=**2954b462-a5de-5af6-83bc-497cc20bddde ** ???????
&client_secret=56V0RnQ1COwhf4YbN9VSkECTKW9sOHsgIuTl1FV9
&grant_type=client_credentials
Run Code Online (Sandbox Code Playgroud)

更新2

我现在已经在ADFS中创建了服务器应用程序,并使用正确的client_id和client_secret对上述有效负载进行POST.

但是,我收到了Object moved一条消息.

RESOLVED BODY: '<html><head><title>Object moved</title></head><body>\r\n<h2>Object moved to <a href="https://fs.our.domain.name/adfs/ls/?wa=wsignin1.0&amp;wtrealm=https%3a%2f%2fmy.crm.endpoint%2f&amp;wctx=http%253a%252f%252f2954b462-a5de-5af6-83bc-497cc20bddde%252f&amp;wct=2018-04-16T13%3a17%3a29Z&amp;wauth=urn%3afederation%3aauthentication%3awindows">here</a>.</h2>\r\n</body></html>\r\n'
Run Code Online (Sandbox Code Playgroud)

问题任何人都可以请描述我做错了什么以及我应该做些什么来正确地验证ADFS/CRM?

注意:当我在浏览器中访问时https://my.crm.endpoint,系统会提示我输入用户名和密码.输入我的信用证,我可以访问CRM.在网络选项卡中注意到它正在使用NTLM来执行此操作?这会改变我需要采取的方法吗?

更新3

在此处查看新问题

An0*_*d3r 2

所以...我设法通过对浏览器的身份验证方法进行逆向工程来实现这一点:)没有代理或Azure废话!

我现在直接使用我们的 fs 端点进行身份验证并解析生成的 SAML 响应并使用它提供的 cookie...这很不错。

注意:下面的代码只是在我的 Node 便签本中敲出来的,所以很混乱。我可能会整理它并在某个时候发布完整的文章,但现在,如果您使用任何此代码,您将需要适当重构;)

let ADFS_USERNAME = '<YOUR_ADFS_USERNAME>'
let ADFS_PASSWORD = '<YOUR_ADFS_PASSWORD>'

let httpntlm = require('httpntlm')
let ntlm = httpntlm.ntlm
let lm = ntlm.create_LM_hashed_password(ADFS_PASSWORD)
let nt = ntlm.create_NT_hashed_password(ADFS_PASSWORD)
let cookieParser = require('set-cookie-parser')
let request = require('request')

let Entity = require('html-entities').AllHtmlEntities
let entities = new Entity()

let uri = 'https://<YOUR_ORGANISATIONS_DOMAIN>/adfs/ls/wia?wa=wsignin1.0&wtrealm=https%3a%2f%2f<YOUR_ORGANISATIONS_CRM_URL>%2f&wctx=rm%3d1%26id%3d1fdab91a-41e8-4100-8ddd-ee744be19abe%26ru%3d%252fdefault.aspx%26crmorgid%3d00000000-0000-0000-0000-000000000000&wct=2019-03-12T11%3a26%3a30Z&wauth=urn%3afederation%3aauthentication%3awindows&client-request-id=e737595a-8ac7-464f-9136-0180000000e1'
let apiUrl = 'https://<YOUR_ORGANISATIONS_CRM_URL>/api/data/v8.2/'
let crm = 'https://<YOUR_ORGANISATIONS_CRM_URL>'

let endpoints = {
  INCIDENTS: `${apiUrl}/incidents?$select=ticketnumber,incidentid,prioritycode,description`,
  CONTACTS: `${apiUrl}/contacts?$select=fullname,contactid`
}

httpntlm.get({
  url: uri,
  username: ADFS_USERNAME,
  lm_password: lm,
  nt_password: nt,
  workstation: '',
  domain: ''
}, function (err, res) {
  if (err) return err
  // this looks messy but is getting the SAML1.0 response ready to pass back as form data in the next request
  let reg = new RegExp('&lt;t:RequestSecurityTokenResponse([\\s\\S]*?)&lt;\/t:RequestSecurityTokenResponse>')
  let result = res.body.match(reg)
  let wresult = entities.decode(result[ 0 ])

  reg = new RegExp('name="wctx" value="([\\s\\S]*?)" /><noscript>')
  result = res.body.match(reg)

  let wctx = entities.decode(result[ 1 ])
  let payload = {
    wctx: wctx,
    wresult: wresult
  }
  getValidCookies(payload)
    .then(cookies => {

      getIncidents(cookies)
        .then(contacts => {
          console.log('GOT INCIDENTS', contacts)
        })
    })
})

getValidCookies = function (payload) {
  return new Promise((resolve, reject) => {

    let options = {
      method: 'POST',
      url: crm,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      form: {
        'wa': 'wsignin1.0',
        'wresult': payload.wresult,
        'wctx': payload.wctx
      }
    }

    request(options, (error, response, body) => {
      let requiredCookies = []
      let cookies = cookieParser.parse(response)

      cookies.forEach(function (cookie) {
        if (cookie.name === 'MSISAuth' || cookie.name === 'MSISAuth1') {
          requiredCookies.push(`${cookie.name}=${cookie.value}`)
        }
      })
      resolve(requiredCookies)
    })

  })
}

getIncidents = function (cookies) {
  return new Promise((resolve, reject) => {

    let options = {
      method: 'GET',
      url: endpoints.INCIDENTS,
      headers: {
        'Cookie': cookies.join(';')
      }
    }

    request(options, (error, response, body) => {
      resolve(body)
    })

  })
}

getContacts = function (cookies) {
  return new Promise((resolve, reject) => {

    let options = {
      method: 'GET',
      url: endpoints.CONTACTS,
      headers: {
        'Cookie': cookies.join(';')
      }
    }

    request(options, (error, response, body) => {
      resolve(body)
    })

  })
}
Run Code Online (Sandbox Code Playgroud)