Использование ADAL C# в качестве конфиденциального пользователя / Демон-сервера / сервер-сервер-401 Unauthorized
ссылка на не отвеченные вопросы:
401 - несанкционированная проверка подлинности с помощью REST API Dynamics CRM с Azure AD
и
и
Dynamics CRM 2016 Online REST API с учетными данными клиента OAuth flow
мне нужна связь между веб-службой в azure cloud и Dynamics CRM Online 2016 без какого-либо экрана входа! Служба будет иметь REST api, который запускает операции CRUD на CRM (также я буду реализовывать аутентификацию)
Я думаю, что это называется " конфиденциальный клиент "или" демон-сервер "или просто"сервер-сервер"
Я правильно настроил свою службу в Azure AD (с помощью "делегировать разрешение = доступ к dynamics online как пользователь организации", других вариантов нет)
Я создал ASP.NET Web API проект в VS, который создал мой веб-сервис в Azure, а также запись ofr "приложение" в Azure AD CRM
мой код выглядит так (PLS игнорирует 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 несанкционированный, хотя я получил и отправил AccessToken.
Какие Идеи?
изменить: Я также попробовал код, рекомендованный в этом блоге (только источник, который, казалось, решил проблему, не работал либо):
этот код:
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!";
}
}
3 ответов
я, наконец, нашел решение. Предоставлено Joao R. В этом посте:
https://community.dynamics.com/crm/f/117/t/193506
прежде всего: забудьте Адала
моя проблема заключалась в том, что я использовал "неправильные" URL-адреса, поскольку вам нужны другие адреса, когда вы не используете Adal (или более общие: перенаправление пользователей).
решение
построить следующий HTTP-Reqest для Токен:
URL: https://login.windows.net/MyCompanyTenant.onmicrosoft.com/oauth2/token
:
- Cache-Control: нет-кэш
- Content-Type: application / x-www-form-urlencoded
тело:
- client_id: YourClientIdFromAzureAd
- ресурс: https://myCompanyTenant.crm.dynamics.com
- имя пользователя: yourServiceUser@myCompanyTenant.onmicrosoft.com
- пароль: yourServiceUserPassword
- grant_type: password
- client_secret: YourClientSecretFromAzureAd
создайте следующий HTTP-запрос для доступа к WebApi:
URL-адрес: https://MyCompanyTenant.api.crm.dynamics.com/api/data/v8.0/accounts
:
- Cache-Control: нет-кэш
- Accept: application / json
- OData-Версия: 4.0
- Авторизация: Предъявитель TokenRetrievedFomRequestAbove
узел.решение 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));
}
(...)
спасибо IntegerWolf за подробный пост / ответ. Я уже потратил много времени, пытаясь подключиться к CRM Web API без каких-либо успехов, пока не наткнулся на ваш пост!
имейте в виду, что ClientId в примере кода является ClientId при регистрации вашего приложения в AAD. Сначала мое соединение не удалось, потому что в объяснении значение для client_id is YourTenantGuid, поэтому я использовал свой Office 365 TenantId, но это должен быть ваш AAD приложение ClientId.
IntegerWolf это определенно указал мне в правильном направлении, но вот то, что в конечном итоге работает для меня:
открывая утверждению
Я запустил следующий код (в помощью linqpad) чтобы определить конечную точку авторизации для экземпляра Dynamics CRM, к которому я хочу подключить Мой демон/службу/приложение:
AuthenticationParameters ap =
AuthenticationParameters.CreateFromResourceUrlAsync(
new Uri(resource + "/api/data/"))
.Result;
return ap.Authority;
resource
- это URL вашего экземпляра CRM (или другого приложения / службы это использование ADAL), например "https://myorg.crm.dynamics.com"
.
в моем случае, возвращаемое значение было "https://login.windows.net/my-crm-instance-tenant-id/oauth2/authorize"
. Я подозреваю, что вы можете просто заменить идентификатор арендатора вашего экземпляра.
источник:
ручная авторизация демона/службы / приложения
это был решающий шаг, для которого я не смог найти помощь.
мне пришлось открыть следующий 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 моего приложения. Но у моего приложения на самом деле нет домашней страницы, поэтому это "не удалось". Но выше, похоже, все еще успешно авторизованы учетные данные моего приложения для доступа к динамике.
приобретая маркер
наконец, код ниже на основе кода в 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);