web-dev-qa-db-ja.com

Webアプリ(ノード/エクスプレス)からオンプレミスのDynamics CRMをクエリする方法

これで頭をいくつかの壁にぶつけて、CRM/Dynamicsの専門家が私に手を貸してくれることを願っています!

Node搭載のExpressアプリ内の管理者資格情報の単一のセットを使用して、Dynamics CRMインスタンスからプログラムでデータを取得しようとしています。このExpressアプリは、CRMがホストされているネットワーク外の別のサーバーでホストされています。次に、アプリはCRMデータを要求、処理、およびアクセスできるログインユーザーに戻します(アプリ内の役割/権限によって制御されます)。つまり、エンドユーザーはExpressアプリにログインするだけで、ログインする必要もありません。アプリがCRMインスタンスにアクセスするためにADFS経由。

私たちのCRMセットアップは、インターネット向け(IFD)に設定されたオンプレミスサーバーです。これはActive Directoryフェデレーションサービスを使用します。内部ネットワーク上のADFSサーバーと通信するネットワークの境界上でフェデレーションサービスを実行するWebアプリケーションプロキシサーバーがあります。 ADFSは、ネットワーク上の(インターネットから)接続しているユーザーをオンプレミスADに対して認証します。プロキシが認証されると、ユーザーはCRMに接続できます。

ハイブリッドデプロイがあるため、オンプレミスのActive DirectoryはAzure ADと同期されます。 O365サービス(オンライン交換、Sharepointなど)は、バックグラウンドでAzure ADを使用します。 Active Directoryを同期するため、ユーザーを1か所で管理するだけで済みます。

CRMにはエンドポイントがあります。 https://my.crm.endpointと私はAzureポータルでアプリ(CRMアプリと呼ばれる)を登録し、ホームページをCRMエンドポイントhttps://my.crm.endpointに設定しました。

質問アプリのホームページをhttps://my.crm.endpointに設定すると、オンプレミスのCRMインスタンスにアプリを「リンク」できますか?

successfullyがAzure Portalに登録されているCRM Appのアクセストークンを要求するスクリプト(crm.js)を作成しました。

トークンの例

eyJ0dWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzE5ZTk1...

ベアラートークンを使用して、通常のエンドポイントを介してDynamicsからいくつかの連絡先を取得しようとします: https://my.crm.endpoint/api/data/v8.2/contacts?$ select = fullname、 contactid

これは失敗し、401 Unauthorisedエラーメッセージが表示されます。

質問誰が問題の可能性を示唆できますか?また、ADFSを使用するオンプレミスサーバー(IFD)で実行されているDynamics CRMに認証済みの要求を行うために、Webアプリ(私の場合はExpress)を接続する方法の詳細を提供しますか?

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

[〜#〜]更新[〜#〜]

@ andresm53のコメントに続いて、ADFSに対して直接認証する必要があると思います。 OAuthで使用できるADFSでの共有シークレットの生成について説明している このブログ投稿 を見つけました。

「この形式のクライアント認証を使用すると、クライアント識別子(client_idとして)とクライアントシークレット(client_secretとして)をSTSエンドポイントにPOSTできます。このようなHTTPの例を次に示しますPOST(Client Credentials Grantを使用して、読みやすいように改行を追加しました): "

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

UPDATE 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'

[〜#〜] question [〜#〜] ADFS/CRMに対して正しく認証するために、誰が私が間違っていることと何をすべきかを説明できますか?

注:ブラウザを使用してhttps://my.crm.endpointにアクセスすると、ユーザー名とパスワードの入力を求められます。資格を入力すると、CRMにアクセスできます。これを行うためにNTLMを使用していることをネットワークタブで確認しましたか?これにより、どのようなアプローチを取る必要がありますか?

更新

新しい質問をご覧ください こちら

20
An0nC0d3r

だから...私はブラウザの認証方法をリバースエンジニアリングすることでこれをなんとか実現しました:)プロキシも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)
    })

  })
}
1
An0nC0d3r

このような状況がありました。私たちの組織はOnPrem 8.2です。 VPNまたはホームネットワークからアクセスできます。非常に基本的な素人の方法で問題を見ると、CRMに外部から到達することはできません。

私たちがしたことは

  1. CRMからWebAPI for Actionを作成しました。
  2. 追加のポートを介してこのWebAPIを外部に公開しました。
  3. このWebAPIをIISにサービスとして追加しました。
  4. ただし、このWebAPIには、Web.configファイルで作成した特定のuserNameおよびPassowordを介してのみアクセスできることを確認しました。
  5. バックグラウンドでは、アクションが作成されました。
  6. 順番にアクションを実行すると、プラグインが実行され、要求されたとおりにデータが返されます。つまり、WebAPIのURLを変更できます。例:..​​./acountsは、ロジックがプラグインに組み込まれている場合、アカウントエンティティに対して返されます。

    これをDynamics CRM OOB WebAPIと混同しないでください。つまり、独自のAPIを作成し、これをサービスとしてIISに独自のユーザー名とパスワードで追加します。

これにより、少なくともどの方向に調べるべきかについてのヒントが得られると思います。

1
AnkUser