6

未回答の質問への言及:

401- Azure AD で REST API Dynamics CRM を使用した不正な認証

Dynamics CRM Online 2016 - デーモン/サーバー アプリケーション Web Api への Azure AD 認証エラー

Dynamics CRM 2016 Online Rest API とクライアント資格情報 OAuth フロー

Azure クラウドの Web サービスと Dynamics CRM Online 2016 の間で、ログイン画面なしで通信する必要があります。このサービスには、CRM で CRUD 操作をトリガーする REST API があります (認証も実装します)。

これは「Confidential Client」または「Daemon Server」または単に「Server-to-Server」と呼ばれていると思います

Azure AD でサービスを適切に設定しました (「委任権限 = 組織ユーザーとしてオンラインでダイナミクスにアクセス」、他にオプションはありません)

VS で ASP.NET WEB API プロジェクトを作成し、Azure で WebService を作成し、CRM の Azure AD 内の「アプリケーション」のエントリも作成しました。

私のコードは次のようになります (エンティティタイプと returnValue は無視してください):

 public class WolfController : ApiController
  {
    private static readonly string Tenant = "xxxxx.onmicrosoft.com";
    private static readonly string ClientId = "dxxx53-42xx-43bc-b14e-c1e84b62752d";
    private static readonly string Password = "j+t/DXjn4PMVAHSvZGd5sptGxxxxxxxxxr5Ki8KU="; // client secret, valid for one or two years
    private static readonly string ResourceId = "https://tenantname-naospreview.crm.dynamics.com/";


    public static async Task<AuthenticationResult> AcquireAuthentificationToken()
    {
      AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/"+ Tenant);
      ClientCredential clientCredentials = new ClientCredential(ClientId, Password);   
      return await authenticationContext.AcquireTokenAsync(ResourceId, clientCredentials);
    }

    // GET: just for calling the DataOperations-method via a GET, ignore the return
    public async Task<IEnumerable<Wolf>> Get()
    {
      AuthenticationResult result = await AcquireAuthentificationToken();
      await DataOperations(result);    

      return new Wolf[] { new Wolf() };
    }


    private static async Task DataOperations(AuthenticationResult authResult)
    {
      using (HttpClient httpClient = new HttpClient())
      {
        httpClient.BaseAddress = new Uri(ResourceId);
        httpClient.Timeout = new TimeSpan(0, 2, 0); //2 minutes
        httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

        Account account = new Account();
        account.name = "Test Account";
        account.telephone1 = "555-555";

        string content = String.Empty;
        content = JsonConvert.SerializeObject(account, new JsonSerializerSettings() {DefaultValueHandling = DefaultValueHandling.Ignore});            

        //Create Entity/////////////////////////////////////////////////////////////////////////////////////
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "api/data/v8.1/accounts");
        request.Content = new StringContent(content);
        request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        HttpResponseMessage response = await httpClient.SendAsync(request);
        if (response.IsSuccessStatusCode)
        {
          Console.WriteLine("Account '{0}' created.", account.name);
        }
        else //Getting Unauthorized here
        {
          throw new Exception(String.Format("Failed to create account '{0}', reason is '{1}'.",account.name, response.ReasonPhrase));
        } ... and more code

GET リクエストを呼び出すと、AccessToken を取得して送信しても 401 Unauthorized が返されます。

何か案は?

編集:このブログでアドバイスされているコードも試しました(問題を解決したように見えるソースのみで、どちらも機能しませんでした):

https://samlman.wordpress.com/2015/06/04/getting-an-azure-access-token-for-a-web-application-entirely-in-code/

このコードで:

public class WolfController : ApiController
  {
    private static readonly string Tenant = System.Configuration.ConfigurationManager.AppSettings["ida:Tenant"];
    private static readonly string TenantGuid = System.Configuration.ConfigurationManager.AppSettings["ida:TenantGuid"];
    private static readonly string ClientId = System.Configuration.ConfigurationManager.AppSettings["ida:ClientID"];
    private static readonly string Password = System.Configuration.ConfigurationManager.AppSettings["ida:Password"]; // client secret, valid for one or two years
    private static readonly string ResourceId = System.Configuration.ConfigurationManager.AppSettings["ida:ResourceID"];

    // GET: api/Wolf
    public async Task<IEnumerable<Wolf>> Get()
    {
      AuthenticationResponse authenticationResponse = await GetAuthenticationResponse();
      String result = await DoSomeDataOperations(authenticationResponse);

      return new Wolf[]
      {
              new Wolf()
              {
                Id = 1,
                Name = result
              }
      };
    }

    private static async Task<AuthenticationResponse> GetAuthenticationResponse()
    {
      //https://samlman.wordpress.com/2015/06/04/getting-an-azure-access-token-for-a-web-application-entirely-in-code/
      //create the collection of values to send to the POST

      List<KeyValuePair<string, string>> vals = new List<KeyValuePair<string, string>>();
      vals.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
      vals.Add(new KeyValuePair<string, string>("resource", ResourceId));
      vals.Add(new KeyValuePair<string, string>("client_id", ClientId));
      vals.Add(new KeyValuePair<string, string>("client_secret", Password));
      vals.Add(new KeyValuePair<string, string>("username", "someUser@someTenant.onmicrosoft.com"));
      vals.Add(new KeyValuePair<string, string>("password", "xxxxxx"));

      //create the post Url   
      string url = string.Format("https://login.microsoftonline.com/{0}/oauth2/token", TenantGuid);

      //make the request
      HttpClient hc = new HttpClient();

      //form encode the data we’re going to POST
      HttpContent content = new FormUrlEncodedContent(vals);

      //plug in the post body
      HttpResponseMessage hrm = hc.PostAsync(url, content).Result;

      AuthenticationResponse authenticationResponse = null;
      if (hrm.IsSuccessStatusCode)
      {
        //get the stream
        Stream data = await hrm.Content.ReadAsStreamAsync();
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof (AuthenticationResponse));
        authenticationResponse = (AuthenticationResponse) serializer.ReadObject(data);
      }
      else
      {
        authenticationResponse = new AuthenticationResponse() {ErrorMessage = hrm.StatusCode +" "+hrm.RequestMessage};
      }

      return authenticationResponse;
    }

    private static async Task<String> DoSomeDataOperations(AuthenticationResponse authResult)
    {
      if (authResult.ErrorMessage != null)
      {
        return "problem getting AuthToken: " + authResult.ErrorMessage;
      }


      using (HttpClient httpClient = new HttpClient())
      {
        httpClient.BaseAddress = new Uri(ResourceId);
        httpClient.Timeout = new TimeSpan(0, 2, 0); //2 minutes
        httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
        httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.access_token);


        //Retreive Entity/////////////////////////////////////////////////////////////////////////////////////
        var retrieveResponse = await httpClient.GetAsync("/api/data/v8.0/feedback?$select=title,rating&$top=10");
        //var retrieveResponse = await httpClient.GetAsync("/api/data/v8.0/$metadata");

        if (!retrieveResponse.IsSuccessStatusCode)
        {
          return retrieveResponse.ReasonPhrase;

        }
        return "it worked!";
      }
    }
4

3 に答える 3

8

私は最終的に解決策を見つけました。Joao R. がこの投稿で提供:

https://community.dynamics.com/crm/f/117/t/193506

まず第一に: ADAL を忘れる

私の問題は、Adal (またはより一般的には user-redirect) を使用していないときに他のアドレスが必要なように見えるため、「間違った」URL を使用していたことです。


解決

トークンに対して次の HTTP 要求を作成します。

URL: https://login.windows.net/MyCompanyTenant.onmicrosoft.com/oauth2/token

ヘッダ:

  • キャッシュ制御: キャッシュなし
  • コンテンツ タイプ: application/x-www-form-urlencoded

体:

  • client_id: YourClientIdFromAzureAd
  • リソース: https://myCompanyTenant.crm.dynamics.com
  • ユーザー名: yourServiceUser@myCompanyTenant.onmicrosoft.com
  • パスワード: yourServiceUserPassword
  • grant_type: パスワード
  • client_secret: YourClientSecretFromAzureAd

WebApi にアクセスするための次の HTTP リクエストを作成します。

URL: https://MyCompanyTenant.api.crm.dynamics.com/api/data/v8.0/accounts

ヘッダ:

  • キャッシュ制御: キャッシュなし
  • 受け入れる: アプリケーション/json
  • OData バージョン: 4.0
  • 承認: Bearer TokenRetrievedFomRequestAbove

Node.js ソリューション (トークンを取得するためのモジュール)

var https = require("https");
var querystring = require("querystring");
var config = require("../config/configuration.js");
var q = require("q");

var authHost = config.oauth.host;
var authPath = config.oauth.path;
var clientId = config.app.clientId;
var resourceId = config.crm.resourceId;
var username = config.crm.serviceUser.name;
var password = config.crm.serviceUser.password;
var clientSecret =config.app.clientSecret;

function retrieveToken() {
    var deferred = q.defer();   
    var bodyDataString = querystring.stringify({
        grant_type: "password",
        client_id:  clientId, 
        resource: resourceId,
        username: username,
        password: password,        
        client_secret: clientSecret
    });
    var options = {
        host: authHost,
        path: authPath,
        method: 'POST',
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "Cache-Control": "no-cache"
        }
    };      
    var request = https.request(options, function(response){
        // Continuously update stream with data
        var body = '';
        response.on('data', function(d) {
            body += d;
        });
        response.on('end', function() {
            var parsed = JSON.parse(body); //todo: try/catch
            deferred.resolve(parsed.access_token);
        });               
    });

    request.on('error', function(e) {
        console.log(e.message);
        deferred.reject("authProvider.retrieveToken: Error retrieving the authToken: \r\n"+e.message);
    });

   request.end(bodyDataString);
   return deferred.promise;    
}

module.exports = {retrieveToken: retrieveToken};

C# ソリューション (トークンの取得と使用)

  public class AuthenticationResponse
  {
    public string token_type { get; set; }
    public string scope { get; set; }
    public int expires_in { get; set; }
    public int expires_on { get; set; }
    public int not_before { get; set; }
    public string resource { get; set; }
    public string access_token { get; set; }
    public string refresh_token { get; set; }
    public string id_token { get; set; }
  }

private static async Task<AuthenticationResponse> GetAuthenticationResponse()
{
  List<KeyValuePair<string, string>> vals = new List<KeyValuePair<string, string>>();

  vals.Add(new KeyValuePair<string, string>("client_id", ClientId));
  vals.Add(new KeyValuePair<string, string>("resource", ResourceId));
  vals.Add(new KeyValuePair<string, string>("username", "yxcyxc@xyxc.onmicrosoft.com"));
  vals.Add(new KeyValuePair<string, string>("password", "yxcycx"));
  vals.Add(new KeyValuePair<string, string>("grant_type", "password"));
  vals.Add(new KeyValuePair<string, string>("client_secret", Password));


  string url = string.Format("https://login.windows.net/{0}/oauth2/token", Tenant);

  using (HttpClient httpClient = new HttpClient())
  {
    httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
    HttpContent content = new FormUrlEncodedContent(vals);
    HttpResponseMessage hrm = httpClient.PostAsync(url, content).Result;

    AuthenticationResponse authenticationResponse = null;
    if (hrm.IsSuccessStatusCode)
    {
      Stream data = await hrm.Content.ReadAsStreamAsync();
      DataContractJsonSerializer serializer = new
    DataContractJsonSerializer(typeof(AuthenticationResponse));
      authenticationResponse = (AuthenticationResponse)serializer.ReadObject(data);
    }
    return authenticationResponse;
  }
}

private static async Task DataOperations(AuthenticationResponse authResult)
{    
  using (HttpClient httpClient = new HttpClient())
  {
    httpClient.BaseAddress = new Uri(ResourceApiId);
    httpClient.Timeout = new TimeSpan(0, 2, 0); //2 minutes
    httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
    httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
    httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache");
    httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.access_token);

    Account account = new Account();
    account.name = "Test Account";
    account.telephone1 = "555-555";

    string content = String.Empty;
    content = JsonConvert.SerializeObject(account, new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore });
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "api/data/v8.0/accounts");
    request.Content = new StringContent(content);
    request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
    HttpResponseMessage response = await httpClient.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
      Console.WriteLine("Account '{0}' created.", account.name);
    }
    else
    {
      throw new Exception(String.Format("Failed to create account '{0}', reason is '{1}'."
        , account.name
        , response.ReasonPhrase));
    }
(...)
于 2016-06-24T08:29:38.313 に答える
0

詳細な投稿/回答を提供してくれたIntegerWolfに感謝します。あなたの投稿に出くわすまで、運がなくても CRM Web API に接続しようとして多くの時間を無駄にしました!

コード サンプルの ClientId は、アプリケーションを AAD に登録するときに提供される ClientId であることに注意してください。説明ではclient_idの値がYourTenantGuidであるため、最初は接続に失敗しました。そのため、Office 365 TenantId を使用しましたが、これは AAD アプリケーションの ClientId である必要があります。

于 2016-07-08T10:13:09.437 に答える
0

IntegerWolfの答えは間違いなく私を正しい方向に向けましたが、最終的に私のために働いたのは次のとおりです:

認可機関の発見

次のコードを ( LINQPadで) 実行して、デーモン/サービス/アプリケーションを接続する Dynamics CRM インスタンスに使用する認証エンドポイントを決定しました。

AuthenticationParameters ap =
    AuthenticationParameters.CreateFromResourceUrlAsync(
                                new Uri(resource + "/api/data/"))
                            .Result;

return ap.Authority;

resourceCRM インスタンス (または ADAL を使用している他のアプリ/サービス) の URL です"https://myorg.crm.dynamics.com"

私の場合、戻り値は でした"https://login.windows.net/my-crm-instance-tenant-id/oauth2/authorize"。インスタンスのテナント ID を単純に置き換えることができると思います。

ソース:

デーモン/サービス/アプリケーションの手動承認

これは、私が助けを見つけることができなかった重要なステップでした.

次の URL を Web ブラウザで開く必要がありました [見やすいようにフォーマットされています]:

https://login.windows.net/my-crm-instance-tenant-id/oauth2/authorize?
   client_id=my-app-id
  &response_type=code
  &resource=https%3A//myorg.crm.dynamics.com

その URL のページが読み込まれると、デーモン/サービス/アプリを実行したいユーザーの資格情報を使用してログインしました。次に、ログインしたユーザーとしてデーモン/サービス/アプリの Dynamics CRM へのアクセスを許可するように求められました。アクセスを許可しました。

login.windows.netサイト/アプリが、アプリの Azure Active Directory 登録で設定したアプリの「ホーム ページ」を開こうとしたことに注意してください。しかし、私のアプリには実際にはホームページがないため、これは「失敗」しました。しかし、上記の方法でも、アプリの資格情報が Dynamics にアクセスすることを正常に承認しているようです。

トークンの取得

最後に、 IntegerWolfの回答のコードに基づいた以下のコードがうまくいきました。

使用されるエンドポイントは、URL パスの最後のセグメントtokenauthorize.

string AcquireAccessToken(
        string appId,
        string appSecretKey,
        string resource,
        string userName,
        string userPassword)
{
    Dictionary<string, string> contentValues =
        new Dictionary<string, string>()
        {
                { "client_id", appId },
                { "resource", resource },
                { "username", userName },
                { "password", userPassword },
                { "grant_type", "password" },
                { "client_secret", appSecretKey }
        };

    HttpContent content = new FormUrlEncodedContent(contentValues);

    using (HttpClient httpClient = new HttpClient())
    {
        httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache");

        HttpResponseMessage response =
            httpClient.PostAsync(
                        "https://login.windows.net/my-crm-instance-tenant-id/oauth2/token",
                        content)
            .Result
            //.Dump() // LINQPad output
            ;

        string responseContent =
                response.Content.ReadAsStringAsync().Result
                //.Dump() // LINQPad output
                ;

        if (response.IsOk() && response.IsJson())
        {
            Dictionary<string, string> resultDictionary =
                (new JavaScriptSerializer())
                .Deserialize<Dictionary<string, string>>(responseContent)
                    //.Dump() // LINQPad output
                    ;

            return resultDictionary["access_token"];
        }
    }

    return null;
}

上記のコードでは、いくつかの拡張メソッドを使用しています。

public static class HttpResponseMessageExtensions
{
    public static bool IsOk(this HttpResponseMessage response)
    {
        return response.StatusCode == System.Net.HttpStatusCode.OK;
    }

    public static bool IsHtml(this HttpResponseMessage response)
    {
        return response.FirstContentTypeTypes().Contains("text/html");
    }

    public static bool IsJson(this HttpResponseMessage response)
    {
        return response.FirstContentTypeTypes().Contains("application/json");
    }

    public static IEnumerable<string> FirstContentTypeTypes(
        this HttpResponseMessage response)
    {
        IEnumerable<string> contentTypes =
             response.Content.Headers.Single(h => h.Key == "Content-Type").Value;

        return contentTypes.First().Split(new string[] { "; " }, StringSplitOptions.None);
    }
}

トークンの使用

クラスで作成されたリクエストでトークンを使用するには、トークンをHttpClient含む認証ヘッダーを追加するだけです。

httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", accessToken);
于 2016-11-21T17:24:33.233 に答える