我尝试使用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 - 未经授权的状态代码。我错过了什么?
答案 0 :(得分:9)
感谢大家的回答。我终于使用ADAL 3访问了Dynamics CRM OData API。
由于许多人在执行此操作时仍遇到问题,请参阅以下步骤:
使用您的Dynamics CRM订阅的Office 365管理员用户登录portal.azure.com
。
转到Azure Active Director \ App注册并添加新应用程序注册
输入"姓名"和"登录网址",网址可以是任何内容(例如https://localhost)
选择您刚刚创建的注册应用,转到设置\键
输入密钥描述,单击保存并复制值(并保留它,因为稍后您将需要它)。同时复制已注册应用程序的应用程序ID。
转到"必需的权限",点击添加,然后选择" Dynamics CRM Online"然后打勾"以组织用户身份访问CRM Online"。
这些步骤使客户端应用程序可以使用您在步骤5中创建的应用程序ID和客户端密钥来访问Dynamics CRM。 您的客户端应用程序现在可以对Azure AD进行身份验证,并具有访问CRM Online的权限。但是,CRM Online并不知道这个"客户端应用程序"或"用户"。如果您尝试访问它,CRM API将响应401。
让CRM了解客户端应用程序"或者"用户",您需要添加应用程序用户。
转到CRM \安全角色,创建新的安全角色或只复制"系统管理员"作用
转到CRM \ Settings \ Security \ Users,创建新用户,将表单更改为"应用程序用户"
使用您在上一步中提供的应用程序ID输入必填字段。保存后,CRM将自动填充Azure AD对象ID和URI。
将用户添加到从上一步创建的安全角色。
现在,您应该可以使用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请求可能会失败。