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:
- client_id: YourClientIdFromAzureAd
- recurso: https://myCompanyTenant.crm.dynamics.com
- nombre de usuario: [email protected]
- contraseña: yourServiceUserPassword
- grant_type: contraseña
- client_secret: YourClientSecretFromAzureAd
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
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ó):
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);