401-使用REST API动态CRM与Azure AD进行未经授权的身份验证

时间:2016-05-13 17:11:49

标签: rest azure oauth-2.0 dynamics-crm adal

我尝试使用Azure AD oAuth 2身份验证访问Dynamics CRM Online REST API。为此,我按照以下步骤操作:

- 我在Azure中注册了一个Web应用程序和/或web api - 将动态CRM的权限配置为具有委派权限"以组织用户身份访问CRM Online"
- 并创建了一个有效期为1年的密钥并保留了客户端ID。

在Azure上配置Web应用程序后,我在.NET / C#中创建了一个使用ADAL发出简单请求的控制台应用程序,在这种情况下检索帐户列表:

    class Program
{
    private static string ApiBaseUrl = "https://xxxxx.api.crm4.dynamics.com/";
    private static string ApiUrl = "https://xxxxx.api.crm4.dynamics.com/api/data/v8.1/";
    private static string ClientId = "2a5dcdaf-2036-4391-a3e5-9d0852ffe3f2";
    private static string AppKey = "symCaAYpYqhiMK2Gh+E1LUlfxbMy5X1sJ0/ugzM+ur0=";

    static void Main(string[] args)
    {
        AuthenticationParameters ap = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(ApiUrl)).Result;

        var clientCredential = new ClientCredential(ClientId, AppKey);

        var authenticationContext = new AuthenticationContext(ap.Authority);
        var authenticationResult = authenticationContext.AcquireToken(ApiBaseUrl, clientCredential);

        var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);

        var result = httpClient.GetAsync(Path.Combine(ApiUrl, "accounts")).Result;         
    }
}

我成功检索了访问令牌,但当我尝试对CRM进行httprequest时,我总是得到 401 - 未经授权的状态代码。我错过了什么?

6 个答案:

答案 0 :(得分:9)

感谢大家的回答。我终于使用ADAL 3访问了Dynamics CRM OData API。

由于许多人在执行此操作时仍遇到问题,请参阅以下步骤:

应用程序注册

  1. 使用您的Dynamics CRM订阅的Office 365管理员用户登录portal.azure.com

  2. 转到Azure Active Director \ App注册并添加新应用程序注册

  3. 输入"姓名"和"登录网址",网址可以是任何内容(例如https://localhost

  4. 选择您刚刚创建的注册应用,转到设置\键

  5. 输入密钥描述,单击保存并复制值(并保留它,因为稍后您将需要它)。同时复制已注册应用程序的应用程序ID。

  6. 转到"必需的权限",点击添加,然后选择" Dynamics CRM Online"然后打勾"以组织用户身份访问CRM Online"。

  7. 这些步骤使客户端应用程序可以使用您在步骤5中创建的应用程序ID和客户端密钥来访问Dynamics CRM。 您的客户端应用程序现在可以对Azure AD进行身份验证,并具有访问CRM Online的权限。但是,CRM Online并不知道这个"客户端应用程序"或"用户"。如果您尝试访问它,CRM API将响应401。

    添加CRM应用程序用户

    让CRM了解客户端应用程序"或者"用户",您需要添加应用程序用户。

    1. 转到CRM \安全角色,创建新的安全角色或只复制"系统管理员"作用

    2. 转到CRM \ Settings \ Security \ Users,创建新用户,将表单更改为"应用程序用户"

    3. 使用您在上一步中提供的应用程序ID输入必填字段。保存后,CRM将自动填充Azure AD对象ID和URI。

    4. 将用户添加到从上一步创建的安全角色。

    5. 现在,您应该可以使用HttpClient和ADAL使用以下示例代码访问CRM API:

      var ap = await AuthenticationParameters.CreateFromResourceUrlAsync(
                      new Uri("https://*****.api.crm6.dynamics.com/api/data/v9.0/"));
      
      String authorityUrl = ap.Authority;
      String resourceUrl = ap.Resource;
      
      var authContext = new AuthenticationContext(authorityUrl);
      var clientCred = new ClientCredential("Application ID", "Client Secret");
      var test = await authContext.AcquireTokenAsync(resourceUrl, clientCred);
      
      Console.WriteLine(test.AccessToken);
      
      using (var client = new HttpClient())
      {
          client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", test.AccessToken);
      
          var response = await client.GetAsync("https://*****.api.crm6.dynamics.com/api/data/v9.0/contacts");
          var contacts = await response.Content.ReadAsStringAsync();
      
          Console.WriteLine(contacts);
      }
      

答案 1 :(得分:4)

1年零2个月后,同样的代码完美无缺。正如许多人所说,Dynamics 365在此期间开始支持服务器到服务器(S2S)身份验证。我不得不做的唯一一步就是创建一个应用程序用户。 有关如何进行此身份验证的详细信息,请访问以下网站:https://msdn.microsoft.com/en-us/library/mt790170.aspx

答案 2 :(得分:3)

我建议您查看服务器到服务器(S2S)身份验证,该身份验证已在最新版本中添加到Dynamics 365中。

通过使用S2S,您不需要付费的Dynamics 365许可证。应用程序基于由Azure AD对象ID值标识的服务主体进行身份验证,而不是用户凭据,该值由存储在Dynamics 365应用程序用户记录中。

可在此处找到更多信息: https://msdn.microsoft.com/en-us/library/mt790168.aspx https://msdn.microsoft.com/en-us/library/mt790170.aspx

答案 3 :(得分:0)

您的ClientId,来自Azure的AppKey,因此ap.Authority https://login.microsoftonline.com/tenantid var authenticationContext = new AuthenticationContext(ap.Authority);应该public class MovieFragment extends Fragment { private GridViewAdapter mGridViewAdapter; private ArrayList<Movie> mMovieList = new ArrayList<Movie>(); public MovieFragment() { } @Override public void onStart() { super.onStart(); FetchMoviesTask movieTask = new FetchMoviesTask(); movieTask.execute("popular"); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Add this line in order for this fragment to handle menu events. setHasOptionsMenu(true); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.moviefragment, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_refresh) { FetchMoviesTask movieTask = new FetchMoviesTask(); movieTask.execute("popular"); return true; } return super.onOptionsItemSelected(item); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.moviefragment, container, false); mGridViewAdapter = new GridViewAdapter(getActivity(),R.layout.movie_list_item, mMovieList); GridView movieGrid= (GridView) rootView.findViewById(R.id.gridview_movie); movieGrid.setAdapter(mGridViewAdapter); return rootView; } public class FetchMoviesTask extends AsyncTask<String, Void, Movie[]> { private final String LOG_TAG = FetchMoviesTask.class.getSimpleName(); private Movie[] getMoviesDataFromJson(String moviesJsonStr) throws JSONException{ final String OMDB_RESULTS="results"; final String OMBD_POSTER_PATH="poster_path"; final String OMBD_RELEASE_DATE="release_date"; final String OMBD_OVERVIEW="overview"; final String OMBD_ORIGINAL_TITLE="original_title"; final String OMBD_VOTE_AVERAGE="vote_average"; final String url= "http://image.tmdb.org/t/p/"; final String imageSize="w185"; JSONObject moviesJson = new JSONObject(moviesJsonStr); JSONArray moviesArray = moviesJson.getJSONArray(OMDB_RESULTS); Movie[] results = new Movie[moviesArray.length()]; //CUIDADO ACA NO SE SI SE PASA POR UNO for (int i=0; i<moviesArray.length();i++){ JSONObject movie=moviesArray.getJSONObject(i); Movie index=new Movie(); index.setPoster_path(url+imageSize+movie.getString(OMBD_POSTER_PATH)); index.setOriginalTitle(movie.getString(OMBD_ORIGINAL_TITLE)); index.setOverview(movie.getString(OMBD_OVERVIEW)); index.setReleaseDate(movie.getString(OMBD_RELEASE_DATE)); index.setVoteAverage(movie.getDouble(OMBD_VOTE_AVERAGE)); results[i]=index; } return results; } @Override protected Movie[] doInBackground(String... params) { Movie[] imageMovies; // If there's no zip code, there's nothing to look up. Verify size of params. if (params.length == 0) { return null; } // These two need to be declared outside the try/catch // so that they can be closed in the finally block. HttpURLConnection urlConnection = null; BufferedReader reader = null; // Will contain the raw JSON response as a string. String moviesJsonStr = null; try { // Construct the URL for the OpenWeatherMap query // Possible parameters are avaiable at OWM's forecast API page, at // http://openweathermap.org/API#forecast final String MOVIE_BASE_URL = "https://api.themoviedb.org/3/movie/"+params[0]; final String APPID_PARAM = "api_key"; Uri builtUri = Uri.parse(MOVIE_BASE_URL).buildUpon() .appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_MOVIEDB_API_KEY) .build(); URL url = new URL(builtUri.toString()); //Log.v(LOG_TAG, "Built URI " + builtUri.toString()); // Create the request to OpenWeatherMap, and open the connection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setRequestMethod("GET"); urlConnection.connect(); // Read the input stream into a String InputStream inputStream = urlConnection.getInputStream(); StringBuffer buffer = new StringBuffer(); if (inputStream == null) { // Nothing to do. return null; } reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { // Since it's JSON, adding a newline isn't necessary (it won't affect parsing) // But it does make debugging a *lot* easier if you print out the completed // buffer for debugging. buffer.append(line + "\n"); } if (buffer.length() == 0) { // Stream was empty. No point in parsing. return null; } moviesJsonStr = buffer.toString(); imageMovies = getMoviesDataFromJson(moviesJsonStr); } catch (IOException e) { Log.e(LOG_TAG, "Error ", e); // If the code didn't successfully get the weather data, there's no point in attemping // to parse it. return null; } catch (JSONException e) { Log.e(LOG_TAG, "Error ", e); // If the code didn't successfully get the weather data, there's no point in attemping // to parse it. return null; } finally { if (urlConnection != null) { urlConnection.disconnect(); } if (reader != null) { try { reader.close(); } catch (final IOException e) { Log.e(LOG_TAG, "Error closing stream", e); } } } return imageMovies; } @Override protected void onPostExecute(Movie[] movies) { if(movies!=null){ mGridViewAdapter.setGridData(Arrays.asList(movies)); } } } }

答案 4 :(得分:0)

我认为您无法为至少某种“集成帐户”提供用户凭据。您可以使用以下内容避免更传统的弹出/重定向OAUTH流:

sizeof(void*)

注意:我使用的是旧版本的ADAL(2.19.208020213),因为using Microsoft.IdentityModel.Clients.ActiveDirectory; using System; using System.IO; using System.Net; namespace ConsoleApplication2 { class Program { private static string API_BASE_URL = "https://<CRM DOMAIN>/"; private static string API_URL = "https://<CRM DOMAIN>/api/data/v8.1/"; private static string CLIENT_ID = "<CLIENT ID>"; static void Main(string[] args) { var userCredential = new UserCredential("<USERNAME>", "<PASSWORD>"); var authContext = new AuthenticationContext("https://login.windows.net/common", false); var result = authContext.AcquireToken(API_BASE_URL, CLIENT_ID, userCredential); var httpClient = HttpWebRequest.CreateHttp(Path.Combine(API_URL, "accounts")); httpClient.Headers.Add(HttpRequestHeader.Authorization, "Bearer:" + result.AccessToken); using (var sr = new StreamReader(httpClient.GetResponse().GetResponseStream())) { Console.WriteLine(sr.ReadToEnd()); } Console.ReadLine(); } } } 参数已从password构造函数中删除。

编辑:CRM现在支持Server to Server Authentication,允许您创建应用程序用户。

答案 5 :(得分:0)

您可能需要在CRM中设置应用程序用户以与Azure应用程序匹配: https://msdn.microsoft.com/en-us/library/mt790170.aspx

虽然您可以在C#中设置Bearer Token,但由于CRM级别权限,对CRM资源的Web请求可能会失败。