Использование ADAL C# в качестве конфиденциального пользователя / Демон-сервера / сервер-сервер-401 Unauthorized

ссылка на не отвеченные вопросы:

401 - несанкционированная проверка подлинности с помощью REST API Dynamics CRM с Azure AD

и

Dynamics CRM Online 2016-ошибка проверки подлинности демона / серверного приложения Azure AD в веб-Api

и

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.

Какие Идеи?

изменить: Я также попробовал код, рекомендованный в этом блоге (только источник, который, казалось, решил проблему, не работал либо):

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!";
      }
    }

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);