6

参考未回答的问题:

401- 使用 REST API Dynamics CRM 和 Azure AD 的未经授权的身份验证

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(我还将实施身份验证)

我认为这被称为“机密客户端”或“守护进程服务器”或只是“服务器到服务器”

我在 Azure AD 中正确设置了我的服务(使用“委托权限 = 以组织用户身份在线访问动态”,没有其他选项)

我在 VS 中创建了一个 ASP.NET WEB API 项目,该项目在 Azure 中创建了我的 WebService,并在 CRM 的 Azure AD 中创建了“应用程序”的条目

我的代码如下所示(请忽略 EntityType 和 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 请求时,我得到了 401 Unauthorized 尽管我得到并发送了 AccessToken。

有任何想法吗?

编辑:我还尝试了此博客中建议的代码(似乎解决了问题的唯一来源,也没有工作):

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

我的问题是我一直在使用“错误”的 URL,因为在不使用 Adal(或更一般地说:用户重定向)时,您似乎需要其他地址。


解决方案

为 Token 构造以下 HTTP-Reqest:

网址: 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
  • 授予类型:密码
  • client_secret:YourClientSecretFromAzureAd

构造以下 HTTP-Request 以访问 WebApi:

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

标题:

  • 缓存控制:无缓存
  • 接受:应用程序/json
  • OData 版本:4.0
  • 授权:Bearer TokenRetrievedFomRequestAbove

Node.js 解决方案(获取Token的模块)

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;

resource是您的 CRM 实例(或使用 ADAL 的其他应用程序/服务)的 URL,例如"https://myorg.crm.dynamics.com".

就我而言,返回值为"https://login.windows.net/my-crm-instance-tenant-id/oauth2/authorize". 我怀疑您可以简单地替换实例的租户 ID。

来源:

手动授权守护进程/服务/应用程序

这是我未能找到任何帮助的关键步骤。

我必须在网络浏览器中打开以下 URL [格式化以便于查看]:

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 路径的最后一段token不是authorize.

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 回答