使用 ADAL C# 作为机密用户/Daemon 服务器/服务器到服务器 - 401 未经授权

Using ADAL C# as Confidential User /Daemon Server /Server-to-Server - 401 Unauthorized

参考未回答的问题:

Dynamics CRM Online 2016 - Daemon / Server application Azure AD authentication error to Web Api

Dynamics CRM 2016 Online Rest API with client credentials OAuth flow

我需要在 azure cloud 中的 Web 服务和 Dynamics CRM Online 2016 之间进行通信,而无需任何登录屏幕!该服务将有一个 REST api,它会触发 CRM 上的 CRUD 操作(我也会实施身份验证)

我认为这叫做 "Confidential Client" 或 "Daemon Server" 或者只是 "Server-to-Server"

我在 Azure AD 中正确设置了我的服务(使用 "delegate permission = access dynamics online as organization user",没有其他选项)

我在 VS 中创建了一个 ASP.NET WEB API 项目,它在 Azure 中创建了我的 WebService,并且还在 CRM 的 Azure AD 中创建了 "Application" 的条目

我的代码如下所示(请忽略 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!";
      }
    }

我终于找到了解决办法。由 Joao R. 提供 Post:

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

首先:忘记 ADAL

我的问题是我一直在使用 "wrong" URLS,因为在不使用 Adal(或更笼统的:user-redirect)时,您似乎需要其他地址。 =17=]


解决方案

为Token构造如下HTTP-Reqest:

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

Header:

  • Cache-Control: no-cache
  • Content-Type: application/x-www-form-urlencoded

Body:

  • client_id: YourClientIdFromAzureAd
  • 资源:https://myCompanyTenant.crm.dynamics.com
  • 用户名:yourServiceUser@myCompanyTenant.onmicrosoft.com
  • 密码:您的服务用户密码
  • grant_type: 密码
  • client_secret: YourClientSecretFromAzureAd

构造如下HTTP-Request用于访问WebApi:

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

Header:

  • Cache-Control: no-cache
  • 接受:application/json
  • OData-Version: 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));
    }
(...)

感谢 IntegerWolf 提供的详细信息 post/answer。我已经浪费了很多时间尝试连接到 CRM Web API,但没有任何运气,直到我 运行 进入您的 post!

请注意,代码示例中的ClientId 是在AAD 中注册您的应用程序时提供的ClientId。起初我的连接失败,因为在解释中 client_id 的值是 YourTenantGuid,所以我使用了我的 Office 365 TenantId,但是这应该是您的 AAD 应用程序 ClientId。

IntegerWolf's answer 确实为我指明了正确的方向,但最终对我有用的是:

发现授权机构

我 运行 以下代码(在 LINQPad 中)确定用于我希望我的 daemon/service/application 连接到的 Dynamics CRM 实例的授权端点:

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

return ap.Authority;

resource 是您的 CRM 实例(或其他使用 ADAL 的 app/service)的 URL,例如"https://myorg.crm.dynamics.com".

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

来源:

手动授权 Daemon/Service/Application

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

我必须在网络浏览器中打开以下 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 的页面加载时,我使用我想要 运行 我的 daemon/service/app 的用户的凭据登录。然后系统提示我以 logged-in 的用户身份为 daemon/service/app g运行t 访问 Dynamics CRM。我 g运行ted 访问。

请注意,login.windows.net site/app 试图打开我在应用程序的 Azure Active Directory 注册中设置的应用程序的 'home page' .但是我的应用程序实际上没有主页,所以这个 'failed'。但以上似乎仍然成功授权我的应用程序的凭据访问 Dynamics。

获取令牌

最后,下面的代码基于 IntegerWolf's answer 中的代码对我有用。

请注意,使用的端点与上一节中描述的 'manual authorization' 基本相同,除了 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 class 发出的请求中使用令牌,只需添加包含令牌的授权 header:

httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", accessToken);