使用ADAL C#作为机密用户/守护程序服务器/服务器到服务器 – 401未经授权

参考未回答的问题:

401-使用REST API动态CRM与Azure AD进行未经授权的身份validation

Dynamics CRM Online 2016 – 守护程序/服务器应用程序Azure AD身份validation错误到Web Api

具有客户端凭据OAuth流的Dynamics CRM 2016 Online Rest API

我需要在azure云中的Web服务和Dynamics CRM Online 2016之间进行通信,而无需任何登录屏幕! 该服务将具有REST api,可触发CRM上的CRUD操作(我也将实现身份validation)

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

我在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 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> 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。

有任何想法吗?

编辑:我也尝试了在这个博客中建议的代码(只有来源似乎解决了问题,也没有工作):

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> Get() { AuthenticationResponse authenticationResponse = await GetAuthenticationResponse(); String result = await DoSomeDataOperations(authenticationResponse); return new Wolf[] { new Wolf() { Id = 1, Name = result } }; } private static async Task 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> vals = new List<KeyValuePair>(); vals.Add(new KeyValuePair("grant_type", "client_credentials")); vals.Add(new KeyValuePair("resource", ResourceId)); vals.Add(new KeyValuePair("client_id", ClientId)); vals.Add(new KeyValuePair("client_secret", Password)); vals.Add(new KeyValuePair("username", "someUser@someTenant.onmicrosoft.com")); vals.Add(new KeyValuePair("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 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.在这篇文章中提供的:

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

首先:忘记ADAL

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


为令牌构造以下HTTP-Reqest:

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

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

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

标题:

  • 缓存控制:无缓存
  • 接受:application / 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#-Solution(获取和使用令牌)

  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 GetAuthenticationResponse() { List> vals = new List>(); vals.Add(new KeyValuePair("client_id", ClientId)); vals.Add(new KeyValuePair("resource", ResourceId)); vals.Add(new KeyValuePair("username", "yxcyxc@xyxc.onmicrosoft.com")); vals.Add(new KeyValuePair("password", "yxcycx")); vals.Add(new KeyValuePair("grant_type", "password")); vals.Add(new KeyValuePair("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的详细发布/回答。 我已经浪费了很多时间试图连接到CRM Web API而没有任何运气,直到我遇到你的post!

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

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。

资源:

  • 在运行时发现权限 – 使用OAuth连接到Microsoft Dynamics 365 Web服务

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

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

我必须在Web浏览器中打开以下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的页面时,我使用我想要运行我的守护程序/ service / app的用户的凭据登录。 然后,系统提示我授予对作为我登录用户的守护程序/服务/应用程序的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 contentValues = new Dictionary() { { "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 resultDictionary = (new JavaScriptSerializer()) .Deserialize>(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 FirstContentTypeTypes( this HttpResponseMessage response) { IEnumerable 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);