如何从 Web 应用程序(Node/Express)查询本地 Dynamics CRM [英] How to query an on-premises Dynamics CRM from a Web App (Node/Express)

查看:16
本文介绍了如何从 Web 应用程序(Node/Express)查询本地 Dynamics CRM的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的头撞到了几堵墙上,所以希望一些 CRM/Dynamics 专家能帮我一把!

Been banging my head against a few walls with this so hoping some CRM/Dynamics experts can give me a hand!

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

I'm trying to programatically obtain data out of our Dynamics CRM instance, using a single set of admin credentials within a Node powered Express app. This Express app is hosted on a separate server outside of our network where CRM is hosted. The app will then request, process and serve CRM data back to any logged in user who has access (controlled by roles/permissions within the app), meaning an end user only has to login into the Express app, and not have to also login via ADFS in order for the app to access the CRM instance.

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

Our CRM set up is an on premise server configured to be internet facing (IFD). This uses Active Directory Federation services. We have Web Application Proxy Servers running federation services on the perimeter of the network that communicate with ADFS servers on the internal network. ADFS authenticates users connecting from outside the network (from internet) against the on prem AD. Once authenticated the proxy allows users to connect through to CRM.

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

Our on prem active directory is synced with Azure AD as we have a hybrid deployment. Any O365 service (exchange online, sharepoint etc) uses Azure AD in the background. We synchronise the Active directory so we only have to manage users in one place.

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

The CRM has an endpoint, e.g. https://my.crm.endpoint and I have registered an app (called CRM App) in the Azure Portal, with the homepage set to the CRM endpoint https://my.crm.endpoint.

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

Question Is setting the app's Homepage to https://my.crm.endpoint enough to "link" it to our on premise CRM instance?

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

I've written a script (crm.js) that successfully requests an access token for my CRM App registered in Azure Portal, using it's App ID.

示例令牌

eyJ0dWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzE5ZTk1...

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

Using the bearer token, I then attempt to get some contacts out of Dynamics via the the usual endpoint: https://my.crm.endpoint/api/data/v8.2/contacts?$select=fullname,contactid

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

This fails and I get a 401 Unauthorised error message.

问题谁能提出问题可能是什么?和/或提供有关如何连接 Web 应用程序(在我的情况下为 Express)以向在使用 ADFS 的本地服务器 (IFD) 上运行的 Dynamics CRM 发出经过身份验证的请求的详细信息?

Question Can anyone suggest what the problem could be? And/or provide details of how you can hook up a Web App (Express in my case) to make authenticated requests to a Dynamics CRM running on an on-premise server (IFD) that uses ADFS?

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;

错误响应

HTTP Error 401 - Unauthorized: Access is denied

附加信息

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

My current solution is based off these docs...

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

更新

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

Following @andresm53's comment, I think I do need to authenticate against ADFS directly. I've found this blog post that describes generating a shared secret in ADFS that can be used with OAuth.

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

"Using this form of Client Authentication, you would POST your client identifier (as client_id) and your client secret (as client_secret) to the STS endpoint. Here is an example of such an HTTP POST (using Client Credentials Grant, added line breaks only for readability):"

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

更新 2

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

I have now created the Server Application in ADFS and am POSTing the above payload with the correct client_id and client_secret.

但是,我收到 Object 已移动 消息.

However, I get an Object moved message.

RESOLVED BODY: '<html><head><title>Object moved</title></head><body>
<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>
</body></html>
'

问题谁能描述我做错了什么以及我应该做什么才能正确地针对 ADFS/CRM 进行身份验证?

QUESTION Can anyone please describe what I am doing wrong and what I should be doing in order to authenticate against ADFS/CRM correctly?

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

NB: When I'm in my browser and visit https://my.crm.endpoint, I get prompted to enter my username and password. Entering my creds works and I get access to CRM. Have noticed in the network tab that it's using NTLM to do this? Does this change what approach I need to take?

更新 3

请在这里查看新问题

推荐答案

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

So... I managed to get this off the ground by reverse engineering the browsers approach to authenticating :) No proxy or Azure nonsense!

我现在直接使用我们的 fs 端点进行身份验证,并解析生成的 SAML 响应并使用它提供的 cookie……这是一种享受.

I am now directly authenticating with our fs endpoint and parsing the resulting SAML response and using the cookie it provides... which works a treat.

注意:下面的代码只是在我的 Node 便笺簿中敲出来的,所以一团糟.我可能会整理它并在某个时候发布完整的文章,但是现在,如果您使用其中的任何代码,您将需要适当地重构;)

NB: The code below was just knocked up in my Node scratch pad, so it's a mess. I might tidy it up and post a full write up at some point, but for now, if you use any of this code you will want to refactor appropriately ;)

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

  })
}

这篇关于如何从 Web 应用程序(Node/Express)查询本地 Dynamics CRM的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆