c# azure asp.net-web-api adal dynamics-crm-2016

Uso de ADAL C#como usuario confidencial/servidor demonio/servidor a servidor-401 no autorizado



azure asp.net-web-api (3)

Finalmente encontré una solución. Proporcionado por Joao R. en este Post:

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

Primero que nada: OLVIDAR ADAL

Mi problema fue todo el tiempo que estaba usando URLS "incorrectas", ya que parece que necesita otras direcciones cuando no estoy usando Adal (o más en general: redirección de usuario).

Solución

Construya el siguiente HTTP-Reqest para el token:

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

Encabezamiento:

  • Control de caché: sin caché
  • Tipo de contenido: application / x-www-form-urlencoded

Cuerpo:

Construya la siguiente solicitud HTTP para el acceso a WebApi:

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

Encabezamiento:

  • Control de caché: sin caché
  • Aceptar: aplicación / json
  • Versión OData: 4.0
  • Autorización: Bearer TokenRetrievedFomRequestAbove

Solución Node.js (Módulo para obtener el 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 # -Solución (Obtención y uso del 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", "[email protected]")); 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)); } (...)

Refiriéndose a las preguntas no respondidas:

401- Autenticación no autorizada mediante REST API Dynamics CRM con Azure AD

y

Dynamics CRM Online 2016: error de autenticación de Azure AD de la aplicación Daemon / Server en la API web

y

API de descanso en línea de Dynamics CRM 2016 con credenciales de cliente OAuth flow

¡Necesito una comunicación entre un servicio web en la nube azul y Dynamics CRM Online 2016 SIN ninguna pantalla de inicio de sesión! El servicio tendrá una API REST que desencadenará operaciones CRUD en el CRM (también implementaré una autenticación)

Creo que esto se llama "Cliente Confidencial" o "Servidor Daemon" o simplemente "Servidor a Servidor"

Configuré mi servicio correctamente en Azure AD (con "delegar permiso = acceso dinámico en línea como usuario de la organización", no hay otras opciones)

Creé un proyecto de API WEB ASP.NET en VS que creó mi servicio web en Azure y también la entrada de la "Aplicación" dentro de Azure AD del CRM

Mi código se ve así (por favor ignore el EntityType y 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

Cuando llamo a mi solicitud GET, recibo el 401 sin autorización, aunque recibí y envío el AccessToken.

¿Algunas ideas?

EDITAR: También probé el código recomendado en este blog (la única fuente que parecía resolver el problema tampoco funcionó):

https://samlman.wordpress.com/2015/06/04/getting-an-azure-access-token-for-a-web-application-entirely-in-code/

Con este código:

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", "[email protected]")); 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!"; } }


Gracias IntegerWolf por la publicación / respuesta detallada. ¡Ya perdí mucho tiempo intentando conectarme a la API web de CRM sin suerte, hasta que encontré tu publicación!

Tenga en cuenta que ClientId en el código de muestra es el ClientId proporcionado al registrar su aplicación en AAD. Al principio mi conexión falló, porque en la explicación el valor de client_id es YourTenantGuid , así que usé mi Office 365 TenantId, pero esta debería ser su aplicación AAD ClientId.


La respuesta de IntegerWolf definitivamente me señaló en la dirección correcta, pero esto es lo que terminó funcionando para mí:

Descubriendo la autoridad de autorización

LINQPad el siguiente código (en LINQPad ) para determinar el punto final de autorización que se usará para la instancia de Dynamics CRM a la que quiero que se conecte mi daemon / servicio / aplicación:

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

resource es la URL de su instancia de CRM (u otra aplicación / servicio que usa ADAL), por ejemplo, "https://myorg.crm.dynamics.com" .

En mi caso, el valor de retorno fue "https://login.windows.net/my-crm-instance-tenant-id/oauth2/authorize" . Sospecho que simplemente puede reemplazar la identificación de inquilino de su instancia.

Fuente:

Autorización manual del daemon / servicio / aplicación

Este fue el paso crucial para el que no pude encontrar ayuda.

Tuve que abrir la siguiente URL en un navegador web [formateado para facilitar la visualización]:

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

Cuando se cargó la página de esa URL, inicié sesión con las credenciales del usuario para el que quería ejecutar mi daemon / service / app. Luego se me solicitó otorgar acceso a Dynamics CRM para el daemon / service / app como usuario para el que inicié sesión. Le concedí acceso.

Tenga en cuenta que el sitio / aplicación login.windows.net intentó abrir la ''página de inicio'' de mi aplicación que configuré en el registro de Azure Active Directory de mi aplicación. Pero mi aplicación en realidad no tiene una página de inicio, por lo que esto ''falló''. Pero lo anterior todavía parece haber autorizado con éxito las credenciales de mi aplicación para acceder a Dynamics.

Adquiriendo un Token

Finalmente, el siguiente código basado en el código de la respuesta de IntegerWolf funcionó para mí.

Tenga en cuenta que el punto final utilizado es principalmente el mismo que para la "autorización manual" descrita en la sección anterior, excepto que el segmento final de la ruta URL es un token lugar de 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; }

El código anterior hace uso de algunos métodos de extensión:

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

Usando un token

Para usar un token con solicitudes realizadas con la clase HttpClient , simplemente agregue un encabezado de autorización que contenga el token:

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