【发布时间】:2019-08-21 15:01:26
【问题描述】:
我正在尝试通过 Microsoft Graph API 检索 Azure AD 用户配置文件数据。我使用来自各种来源(主要是 Microsoft)的代码示例设置了一个小型 Visual Studio MVC 应用程序。在我的无知中,我认为这将是一个相当简单的过程。
我在 SO 上浏览过其他类似的案例,并试图利用其他人的建议,但无济于事。我已经解决了这个问题四天,非常感谢任何帮助。
// UserProfileController.cs
-- contains the calling method: var graphToken = await AuthenticationHelper.GetGraphAccessToken();
//
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using SSO_Test.Models;
using SSO_Test.Utils;
using System.Net.Http.Headers;
namespace SSO_Test.Controllers
{
[Authorize]
public class UserProfileController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
private string clientId = ConfigurationManager.AppSettings["ClientId"];
private string appKey = ConfigurationManager.AppSettings["ClientSecret"];
private string aadInstance = ConfigurationManager.AppSettings["AADInstance"];
private string graphResourceID = "https://graph.microsoft.com";
// GET: UserProfile
public async Task<ActionResult> Index()
{
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;
try
{
var graphToken = await AuthenticationHelper.GetGraphAccessToken();
var authenticationProvider = new DelegateAuthenticationProvider(
(requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", graphToken);
return Task.FromResult(0);
});
var graphClient = new GraphServiceClient(authenticationProvider);
var user = await graphClient.Me.Request().GetAsync();
return View(user);
}
catch (AdalException ex)
{
// Return to error page.
ViewBag.Message = ex.Message;
return View("Error");
}
// if the above failed, the user needs to explicitly re-authenticate for the app to obtain the required token
catch (Exception)
{
return View("Relogin");
}
}
public void RefreshSession()
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/Home" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
}
//AuthenticationHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using SSO_Test.Models;
namespace SSO_Test.Utils
{
public static class AuthenticationHelper
{
public static async Task<string> GetGraphAccessToken()
{
var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var userObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimTypeObjectIdentifier).Value;
var clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
var userIdentifier = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);
// create auth context
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureAdAuthority, new ADALTokenCache(signInUserId));
//added check point for verification purposes
System.Diagnostics.Debug.WriteLine("Check point #1");
//GOOD TO THIS POINT
var result = await authContext.AcquireTokenSilentAsync(SettingsHelper.AzureAdGraphResourceURL, clientCredential, userIdentifier);
//ERROR MESSAGE: "Failed to acquire token silently as no token was found in the cache. Call method AcquireToken"
System.Diagnostics.Debug.WriteLine("Check point #2");
//app never reaches the second check point
return result.AccessToken;
}
}
}
//ADALTokenCache.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Security;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace SSO_Test.Models
{
public class ADALTokenCache : TokenCache
{
private ApplicationDbContext db = new ApplicationDbContext();
private string userId;
private UserTokenCache Cache;
public ADALTokenCache(string signedInUserId)
{
// associate the cache to the current user of the web app
userId = signedInUserId;
this.BeforeAccess = BeforeAccessNotification;
this.AfterAccess = AfterAccessNotification;
this.BeforeWrite = BeforeWriteNotification;
// look up the entry in the database
Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
// place the entry in memory
this.DeserializeAdalV3((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache"));
}
// clean up the database
public override void Clear()
{
base.Clear();
var cacheEntry = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
db.UserTokenCacheList.Remove(cacheEntry);
db.SaveChanges();
}
// Notification raised before ADAL accesses the cache.
// This is your chance to update the in-memory copy from the DB, if the in-memory version is stale
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
if (Cache == null)
{
// first time access
Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
}
else
{
// retrieve last write from the DB
var status = from e in db.UserTokenCacheList
where (e.webUserUniqueId == userId)
select new
{
LastWrite = e.LastWrite
};
// if the in-memory copy is older than the persistent copy
if (status.First().LastWrite > Cache.LastWrite)
{
// read from from storage, update in-memory copy
Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == userId);
}
}
this.DeserializeAdalV3((Cache == null) ? null : MachineKey.Unprotect(Cache.cacheBits, "ADALCache"));
}
// Notification raised after ADAL accessed the cache.
// If the HasStateChanged flag is set, ADAL changed the content of the cache
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if state changed
if (this.HasStateChanged)
{
Cache = new UserTokenCache
{
webUserUniqueId = userId,
//cacheBits = MachineKey.Protect(this.Serialize(), "ADALCache"),
cacheBits = MachineKey.Protect(this.SerializeAdalV3(), "ADALCache"),
LastWrite = DateTime.Now
};
// update the DB and the lastwrite
db.Entry(Cache).State = Cache.UserTokenCacheId == 0 ? EntityState.Added : EntityState.Modified;
db.SaveChanges();
this.HasStateChanged = false;
}
}
void BeforeWriteNotification(TokenCacheNotificationArgs args)
{
// if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
}
}
}
//ApplicationDbContext.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace SSO_Test.Models
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
public DbSet<UserTokenCache> UserTokenCacheList { get; set; }
}
public class UserTokenCache
{
[Key]
public int UserTokenCacheId { get; set; }
public string webUserUniqueId { get; set; }
public byte[] cacheBits { get; set; }
public DateTime LastWrite { get; set; }
}
}
如您所见,我在 GetGraphAccessToken() 方法中记录了错误消息:
“由于在缓存中未找到令牌,因此无法静默获取令牌。 调用方法 AcquireToken"。
我能够将 AcquireTokenSilentAsync() 方法隔离为罪魁祸首,方法是用一对 Debug.Writeline 语句括起来,第一个语句运行成功,第二个语句没有运行。这已通过查看 VS 输出窗口的内容得到验证,如下所示:
检查点 #1
抛出异常: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalSilentTokenAcquisitionException' 在 mscorlib.dll 中
程序“[13980] iisexpress.exe”已退出,代码为 -1 (0xffffffff)。
我真的希望这个东西能够工作,我更喜欢使用 Graph SDK 方法而不是使用 REST API。
再一次,我已经用头撞墙了四天多。我的头还好,但墙的形状不好。
提前致谢。
【问题讨论】:
-
我注意到您正在使用 ADAL。您会考虑使用 MSAL 吗?如果可以,我建议您查看this MSAL sample。
-
我下载并运行了示例,没有任何问题。问题是,我还没有为 ASP.Net Core 做好准备。但是,我能够从示例中收集到足够的信息来改进我的 .Net 4.6 应用程序以利用基于 MSAL 的缓存。
标签: c# azure-active-directory microsoft-graph-api azure-ad-b2c microsoft-graph-sdks