c# - 使用 ADAL C# 作为 secret 用户/守护程序服务器/服务器到服务器 - 401 未经授权

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

引用未回答的问题:

401- Unauthorized authentication using REST API Dynamics CRM with Azure AD

Dynamics CRM Online 2016 - Daemon / Server application Azure AD authentication error to Web Api

Dynamics CRM 2016 Online Rest API with client credentials OAuth flow

我需要在 azure 云中的 Web 服务与 Dynamics CRM Online 2016 之间进行通信,而无需任何登录屏幕!该服务将有一个 REST api,它会触发 CRM 上的 CRUD 操作(我还将实现身份验证)

我认为这称为“ secret 客户端”或“守护程序服务器”或只是“服务器到服务器”

我在 Azure AD 中正确设置了我的服务(使用“委托(delegate)权限 = 作为组织用户在线访问动态”,没有其他选项)

我在 VS 中创建了一个 ASP.NET WEB API 项目,该项目在 Azure 中创建了我的 WebService 以及 CRM 的 Azure AD 中的“应用程序”条目

我的代码如下所示(请忽略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 请求时,尽管我获取并发送了 AccessToken,但我收到了 401 Unauthorized。

有什么想法吗?

编辑: 我还尝试了此博客中建议的代码(似乎解决问题的唯一来源,但也不起作用):

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", "<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="186b77757d4d6b7d6a586b77757d4c7d7679766c36777675717b6a776b777e6c367b7775" rel="noreferrer noopener nofollow">[email protected]</a>"));
      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!";
      }
    }

最佳答案

终于找到解决办法了。由 Joao R. 在这篇文章中提供:

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

首先:忘记 ADAL

我的问题是我一直在使用“错误”的URLS,因为在不使用Adal(或更一般的:用户重定向)时,您似乎需要其他地址。

<小时/>

解决方案

为 token 构建以下 HTTP 请求:

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

标题:

  • 缓存控制:无缓存
  • 内容类型:application/x-www-form-urlencoded

正文:

构建以下 HTTP 请求来访问 WebApi:

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

标题:

  • 缓存控制:无缓存
  • 接受:application/json
  • OData 版本:4.0
  • 授权:Bearer TokenRetrievedFomRequestAbove
<小时/>

Node.js解决方案(获取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#-解决方案(获取和使用 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", "<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3b4243584243587b43424358155455565258495448545d4f15585456" rel="noreferrer noopener nofollow">[email protected]</a>"));
  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));
    }
(...)

关于c# - 使用 ADAL C# 作为 secret 用户/守护程序服务器/服务器到服务器 - 401 未经授权,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37454539/

相关文章:

c# - 我可以从主构造函数关闭程序吗?

sqlite - 部署到 Azure、SQLite/.NET Core 时执行迁移

azure - 无法使用 Microsoft graph API 获取照片

c# - ASP .NET Webhook 接收器

c# - 如何在 ASP.NET Web API 实现中将数组传递给 OData 函数?

c# - Mono .Net 支持异步等待吗?

c# - 将数字转换为Barcode Ean 13并将其作为图像保存到sql中

c# - 使用索引查找字符串中的任何单词

c# - 如何在长时间运行的 *UI* 操作中让 UI 刷新

c# - Entity Framework /Azure Web服务模型设计