可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Refering to not answered Questions:
401- Unauthorized authentication using REST API Dynamics CRM with Azure AD
and
Dynamics CRM Online 2016 - Daemon / Server application Azure AD authentication error to Web Api
and
Dynamics CRM 2016 Online Rest API with client credentials OAuth flow
I need a communication between an Web-Service in azure cloud and Dynamics CRM Online 2016 WITHOUT any loginscreen! The service will have a REST api which triggers CRUD operations on the CRM (also I will implement an authentification)
I think this is called "Confidential Client" or "Daemon Server" or just "Server-to-Server"
I set up my Service properly in Azure AD (with "delegate permission = access dynamics online as organization user", there are no other options)
I created a ASP.NET WEB API project in VS which created my WebService in Azure and also the entry ofr the "Application" within Azure AD of the CRM
My Code looks like this (pls ignore the EntityType and 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
When calling my GET request I get the 401 Unauthorized although I got and send the AccessToken.
Any Ideas?
EDIT:
I also tried the code adviced in this blog (only source which seemed to solve the problem, didnt work either):
https://samlman.wordpress.com/2015/06/04/getting-an-azure-access-token-for-a-web-application-entirely-in-code/
With this 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!";
}
}
回答1:
I finally found a solution. Provided by Joao R. in this Post:
https://community.dynamics.com/crm/f/117/t/193506
First of all: FORGET ADAL
My problem was the whole time that I was using "wrong" URLS as it seems you need other adresses when not using Adal (or more general: user-redirect).
Solution
Construct following HTTP-Reqest for the Token:
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
- resource: https://myCompanyTenant.crm.dynamics.com
- username: yourServiceUser@myCompanyTenant.onmicrosoft.com
- password: yourServiceUserPassword
- grant_type: password
- client_secret: YourClientSecretFromAzureAd
Construct the following HTTP-Request for the access to WebApi:
URL: https://MyCompanyTenant.api.crm.dynamics.com/api/data/v8.0/accounts
Header:
- Cache-Control: no-cache
- Accept: application/json
- OData-Version: 4.0
- Authorization: Bearer TokenRetrievedFomRequestAbove
Node.js Solution (Module for getting the 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#-Solution (Getting and using the Token)
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));
}
(...)
回答2:
Thanks IntegerWolf for the detailed post/answer. I already wasted a lot of time trying to connect to the CRM Web API without any luck, until I ran into your post!
Please be aware that ClientId in the code sample is the ClientId provided when registering your application in AAD. At first my connection failed, because in the explanation the value for client_id is YourTenantGuid, so I used my Office 365 TenantId, but this should be your AAD application ClientId.
回答3:
IntegerWolf's answer definitely pointed me in the right direction, but here's what ended up working for me:
Discovering the Authorization Authority
I ran the following code (in LINQPad) to determine the authorization endpoint to use for the Dynamics CRM instance to which I want my daemon/service/application to connect:
AuthenticationParameters ap =
AuthenticationParameters.CreateFromResourceUrlAsync(
new Uri(resource + "/api/data/"))
.Result;
return ap.Authority;
resource
is the URL of your CRM instance (or other app/service that's using ADAL), e.g. "https://myorg.crm.dynamics.com"
.
In my case, the return value was "https://login.windows.net/my-crm-instance-tenant-id/oauth2/authorize"
. I suspect you can simply replace the tenant ID of your instance.
Source:
- Discover the authority at run time – Connect to Microsoft Dynamics 365 web services using OAuth
Manually Authorizing the Daemon/Service/Application
This was the crucial step for which I failed to find any help.
I had to open the following URL in a web browser [formatted for easier viewing]:
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
When the page for that URL loaded, I logged in using the credentials for the user for which I wanted to run my daemon/service/app. I was then prompted to grant access to Dynamics CRM for the daemon/service/app as the user for which I logged-in. I granted access.
Note that the login.windows.net site/app tried to open the 'home page' of my app that I setup in my app's Azure Active Directory registration. But my app doesn't actually have a home page so this 'failed'. But the above still seems to have successfully authorized my app's credentials to access Dynamics.
Acquiring a Token
Finally, the code below based on the code in IntegerWolf's answer worked for me.
Note that the endpoint used is mostly the same as for the 'manual authorization' described in the previous section except that the final segment of the URL path is token
instead of 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;
}
The code above makes use of some extension methods:
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);
}
}
Using a Token
To use a token with requests made with the HttpClient
class, just add an authorization header containing the token:
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);