在 MVC Web App 中通过 ViewModel 调用 Graph API
Calling Graph API through ViewModel in MVC Web App
我正在尝试使用 Graph API 创建我自己的网络应用程序导航栏的 "User Profile" 部分。为此,我对 UserProfile 控制器的 GetUser 操作进行了 AJAX 调用:
$.ajax({
type: "GET",
url: "@Url.Action("GetUser", "UserProfile", null)",
dataType: "json",
success: function (data, status, xhr) {
console.log("in AJAX");
$(".img-circle, .user-image").attr("src", data.Picture);
$("#user-menu-expanded").text(data.User.DisplayName + " - " + data.User.JobTitle);
$("#user-menu-spinner").remove();
console.log(data);
},
error: function (ex) {
console.log(ex);
}
});
控制器 returns 我的 UserProfileViewModel 作为 Json 我用它来替换上述元素,如我的 AJAX 成功函数所示。
用户配置文件控制器:
public JsonResult GetUser()
{
var model = new UserProfileViewModel();
return Json(model, JsonRequestBehavior.AllowGet);
}
我的 UserProfileViewModel 如下所示:
public UserProfileViewModel()
{
var graphClient = GetAuthGraphClient();
GetPicture(graphClient);
GetUserProfile(graphClient);
}
public GraphServiceClient GetAuthGraphClient()
{
string graphResourceID = "https://graph.microsoft.com/";
return new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) =>
{
string accessToken = GetTokenForApplication(graphResourceID);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
return Task.FromResult(0);
}
));
}
public string GetTokenForApplication(string graphResourceID)
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = "https://login.microsoftonline.com/" + tenantID;
try {
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(authority);
var token = authenticationContext.AcquireTokenAsync(graphResourceID, clientcred).Result.AccessToken;
return token;
}
catch (Exception e)
{
// Capture error for handling outside of catch block
ErrorMessage = e.Message;
return null;
}
}
public void GetPicture(GraphServiceClient graphClient)
{
Stream photo = Task.Run(async () => { return await graphClient.Me.Photo.Content.Request().GetAsync(); }).Result;
using (var memoryStream = new MemoryStream())
{
photo.CopyTo(memoryStream);
var base64pic = Convert.ToBase64String(memoryStream.ToArray());
this.Picture = "data:image;base64," + base64pic;
HttpContext.Current.Cache.Add("Pic", this.Picture, null, DateTime.Now.AddHours(5), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null);
}
}
public void GetUserProfile(GraphServiceClient graphClient)
{
this.User = Task.Run(async () => { return await graphClient.Me.Request().GetAsync(); }).Result;
}
我成功获得了访问令牌,但是我的 AJAX 调用没有返回任何数据。
Access Token from IIS Log
Console Log
我有两个问题(可能是3个):
- 我做错了什么?
是否可以使用访问令牌
从我的 Startup.Auth 创建一个经过身份验证的 Graph 客户端?如果是这样,
我该怎么做呢?
// This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API.
string graphResourceId = "https://graph.microsoft.com"; //https://graph.windows.net
public void ConfigureAuth(IAppBuilder app)
{
ApplicationDbContext db = new ApplicationDbContext();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = Authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
HttpContext.Current.Cache.Add("Token", result.AccessToken, null, DateTime.Now.AddHours(5), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null);
return Task.FromResult(0);
}
}
});
}
}
根据下方评论更新代码
public string GetTokenForApplication(string graphResourceID)
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = "https://login.microsoftonline.com/" + tenantID;
try {
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(userObjectID));
var result = authenticationContext.AcquireTokenSilent(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (Exception e)
{
// Capture error for handling outside of catch block
ErrorMessage = e.Message;
return null;
}
}
更新 2:修复.. 有点
感谢@Fei Xue 我解决了问题..有点。这解决了我在本地 运行 时的问题,但在发布到我的阶段应用程序时我仍然无法静默获取令牌。当我第一次创建应用程序时,我包含了 Work/School Azure AD 身份验证。这创建了一个用于 ADAL 令牌缓存的本地数据库上下文。在开发应用程序时,我为我为应用程序创建的 Azure SQL 数据库创建了另一个数据库上下文。我必须更新我的 AdalTokenCache.cs 以反映我的应用程序的数据库上下文和新模型。我更新了行:
private ApplicationDbContext db = new ApplicationDbContext();
使用我自己的上下文并将 UserTokenCache 模型更新为我的新上下文的 UserTokenCache 模型。在这种情况下我改变了:
private UserTokenCache Cache;
至:
private UserTokenCach Cache;
然后我更新了 CS 的其余部分以匹配应用程序数据库上下文中的 UserTokenCach。
然后我只使用了 UserProfile 控制器中 OOB 中的 AcquireToken 方法来获取令牌。这就是它最终的样子(注意:我还将 startup.auth 中的字符串从私有更新为 public,以便我可以在我的视图模型中使用它们):
public string GetTokenForApplication(string graphResourceID)
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = "https://login.microsoftonline.com/" + tenantID;
try {
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(Startup.clientId, Startup.appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(signedInUserID));
var result = authenticationContext.AcquireTokenSilent(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (Exception e)
{
// Capture error for handling outside of catch block
ErrorMessage = e.Message;
return null;
}
}
我会在玩更多游戏时更新。
Azure Active Directory 颁发了两种访问令牌。
第一个是delegate-token,用于委托用户操作用户的资源。
还有一个是application token,通常用于对所有组织的资源执行操作,这个token中没有用户上下文。所以我们不应该使用这个令牌来执行需要用户上下文的 me
资源。
post 中的代码是使用客户端凭据流获取访问令牌,即 应用程序令牌。因此,当您根据用户的上下文使用这种令牌获取用户或图片时,您会出错。
在这种情况下,您应该像 post 一样使用 AuthorizationCodeReceived
事件获取访问令牌。此事件使用 授权码授予流程 为用户获取 delegate-token。然后在控制器中,您可以使用方法 AcquireTokenSilentAsync
获取令牌,该方法将从 catch.
获取访问令牌
下面的代码示例对于在 Web 应用中调用 Microsoft Graph 以委托登录用户的场景非常有帮助:
我正在尝试使用 Graph API 创建我自己的网络应用程序导航栏的 "User Profile" 部分。为此,我对 UserProfile 控制器的 GetUser 操作进行了 AJAX 调用:
$.ajax({
type: "GET",
url: "@Url.Action("GetUser", "UserProfile", null)",
dataType: "json",
success: function (data, status, xhr) {
console.log("in AJAX");
$(".img-circle, .user-image").attr("src", data.Picture);
$("#user-menu-expanded").text(data.User.DisplayName + " - " + data.User.JobTitle);
$("#user-menu-spinner").remove();
console.log(data);
},
error: function (ex) {
console.log(ex);
}
});
控制器 returns 我的 UserProfileViewModel 作为 Json 我用它来替换上述元素,如我的 AJAX 成功函数所示。
用户配置文件控制器:
public JsonResult GetUser()
{
var model = new UserProfileViewModel();
return Json(model, JsonRequestBehavior.AllowGet);
}
我的 UserProfileViewModel 如下所示:
public UserProfileViewModel()
{
var graphClient = GetAuthGraphClient();
GetPicture(graphClient);
GetUserProfile(graphClient);
}
public GraphServiceClient GetAuthGraphClient()
{
string graphResourceID = "https://graph.microsoft.com/";
return new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) =>
{
string accessToken = GetTokenForApplication(graphResourceID);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
return Task.FromResult(0);
}
));
}
public string GetTokenForApplication(string graphResourceID)
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = "https://login.microsoftonline.com/" + tenantID;
try {
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(authority);
var token = authenticationContext.AcquireTokenAsync(graphResourceID, clientcred).Result.AccessToken;
return token;
}
catch (Exception e)
{
// Capture error for handling outside of catch block
ErrorMessage = e.Message;
return null;
}
}
public void GetPicture(GraphServiceClient graphClient)
{
Stream photo = Task.Run(async () => { return await graphClient.Me.Photo.Content.Request().GetAsync(); }).Result;
using (var memoryStream = new MemoryStream())
{
photo.CopyTo(memoryStream);
var base64pic = Convert.ToBase64String(memoryStream.ToArray());
this.Picture = "data:image;base64," + base64pic;
HttpContext.Current.Cache.Add("Pic", this.Picture, null, DateTime.Now.AddHours(5), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null);
}
}
public void GetUserProfile(GraphServiceClient graphClient)
{
this.User = Task.Run(async () => { return await graphClient.Me.Request().GetAsync(); }).Result;
}
我成功获得了访问令牌,但是我的 AJAX 调用没有返回任何数据。
Access Token from IIS Log
Console Log
我有两个问题(可能是3个):
- 我做错了什么?
是否可以使用访问令牌 从我的 Startup.Auth 创建一个经过身份验证的 Graph 客户端?如果是这样, 我该怎么做呢?
// This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API. string graphResourceId = "https://graph.microsoft.com"; //https://graph.windows.net public void ConfigureAuth(IAppBuilder app) { ApplicationDbContext db = new ApplicationDbContext(); app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); app.UseKentorOwinCookieSaver(); app.UseCookieAuthentication(new CookieAuthenticationOptions()); app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = clientId, Authority = Authority, PostLogoutRedirectUri = postLogoutRedirectUri, Notifications = new OpenIdConnectAuthenticationNotifications() { // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential(clientId, appKey); string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; AuthenticationContext authContext = new AuthenticationContext(Authority, new ADALTokenCache(signedInUserID)); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId); HttpContext.Current.Cache.Add("Token", result.AccessToken, null, DateTime.Now.AddHours(5), Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null); return Task.FromResult(0); } } }); } }
根据下方评论更新代码
public string GetTokenForApplication(string graphResourceID)
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = "https://login.microsoftonline.com/" + tenantID;
try {
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(userObjectID));
var result = authenticationContext.AcquireTokenSilent(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (Exception e)
{
// Capture error for handling outside of catch block
ErrorMessage = e.Message;
return null;
}
}
更新 2:修复.. 有点
感谢@Fei Xue 我解决了问题..有点。这解决了我在本地 运行 时的问题,但在发布到我的阶段应用程序时我仍然无法静默获取令牌。当我第一次创建应用程序时,我包含了 Work/School Azure AD 身份验证。这创建了一个用于 ADAL 令牌缓存的本地数据库上下文。在开发应用程序时,我为我为应用程序创建的 Azure SQL 数据库创建了另一个数据库上下文。我必须更新我的 AdalTokenCache.cs 以反映我的应用程序的数据库上下文和新模型。我更新了行:
private ApplicationDbContext db = new ApplicationDbContext();
使用我自己的上下文并将 UserTokenCache 模型更新为我的新上下文的 UserTokenCache 模型。在这种情况下我改变了:
private UserTokenCache Cache;
至:
private UserTokenCach Cache;
然后我更新了 CS 的其余部分以匹配应用程序数据库上下文中的 UserTokenCach。
然后我只使用了 UserProfile 控制器中 OOB 中的 AcquireToken 方法来获取令牌。这就是它最终的样子(注意:我还将 startup.auth 中的字符串从私有更新为 public,以便我可以在我的视图模型中使用它们):
public string GetTokenForApplication(string graphResourceID)
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string authority = "https://login.microsoftonline.com/" + tenantID;
try {
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(Startup.clientId, Startup.appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(signedInUserID));
var result = authenticationContext.AcquireTokenSilent(graphResourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (Exception e)
{
// Capture error for handling outside of catch block
ErrorMessage = e.Message;
return null;
}
}
我会在玩更多游戏时更新。
Azure Active Directory 颁发了两种访问令牌。
第一个是delegate-token,用于委托用户操作用户的资源。
还有一个是application token,通常用于对所有组织的资源执行操作,这个token中没有用户上下文。所以我们不应该使用这个令牌来执行需要用户上下文的 me
资源。
post 中的代码是使用客户端凭据流获取访问令牌,即 应用程序令牌。因此,当您根据用户的上下文使用这种令牌获取用户或图片时,您会出错。
在这种情况下,您应该像 post 一样使用 AuthorizationCodeReceived
事件获取访问令牌。此事件使用 授权码授予流程 为用户获取 delegate-token。然后在控制器中,您可以使用方法 AcquireTokenSilentAsync
获取令牌,该方法将从 catch.
下面的代码示例对于在 Web 应用中调用 Microsoft Graph 以委托登录用户的场景非常有帮助: