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

问题描述 投票:20回答:2

一直在用几个墙撞到我的头上,所以希望一些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...

然后,我使用承载令牌尝试通过通常的端点从动态获取一些联系人: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: '<[email protected]>',
    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

附加信息

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

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

UPDATE

根据@ andresm53的评论,我认为我确实需要直接对ADFS进行身份验证。我发现this blog post描述了在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

更新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'

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

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

更新3

请参阅新问题here

node.js dynamics-crm azure-active-directory microsoft-dynamics adfs
2个回答
1
投票

我们有类似的情况。我们的组织是OnPrem 8.2。可通过VPN或家庭网络访问。如果你以非常基本的外行的方式看问题,我们的CRM无法从外部联系到。

我们做的是

  1. 我们从CRM创建了Web API for Action。
  2. 我们通过额外的端口将此WebAPI暴露给外部世界。
  3. 我们在IIS中将此Web API添加为服务。
  4. 但我们确保只能通过我们在Web.config文件中创建的特定用户名和密码来访问此Web API。
  5. 在背景中我们所做的是创建了Action。
  6. Action in Turn将运行插件,并将根据要求返回数据,即可以修改WebAPI URL。例如:... / acounts将返回Account实体提供您的插件中内置逻辑。 请不要将此与Dynamics CRM OOB WebAPI混淆。我的意思是创建我们自己的API并将其作为服务添加到IIS中,并使用自己的用户名和密码。

我认为这至少会给你一些提示,以便在哪个方向上进行研究。


1
投票

所以...我设法通过逆向工程改进浏览器的方法来实现这一点:)没有代理或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)
    })

  })
}
© www.soinside.com 2019 - 2024. All rights reserved.