提前感谢您提供的任何帮助。我在 ASP.NET MVC 应用程序中实现了 OpenID 连接,它旨在访问 Microsoft Graph 并获取从其他服务加载的某些用户的用户个人资料图片。
前端是在 AngularJS 中构建的,加载用户信息后,我将使用用户电子邮件调用 ASP.NET MVC 方法,该方法试图从 Microsoft Graph 获取用户个人资料图片。 openid connect 实现仅在 ASP.NET MVC 应用程序中完成,而不是在 Angular 中完成。
生成的错误详情
catch (Exception ex)
{
encodedPhoto = "ErrorGettingPhoto" + ex.ToString();
}
说的是:
ErrorGettingPhoto System.AggregateException:发生一个或多个错误。Microsoft.Graph.ServiceException:代码:generalException
消息:发送请求时出错。
System.NullReferenceException:对象引用未设置到对象的实例。在 F:\AzureAgent\_work\94\s\Helpers\TokenHelper.cs 中的 Web.Helpers.TokenHelper.GetUsersUniqueId(ClaimsPrincipal 用户):第 113 行
在 F:\AzureAgent\_work\94\s\Web\Helpers\TokenHelper.cs:line 33
在 Web.Helpers.GraphHelper.GetAuthenticatedClient\u003eb__10_0\u003ed.MoveNext() 在 F:\AzureAgent\_work\94\s\Web\Helpers\GraphHelper.cs:line 96
StartUp
类具有以下结构:
public partial class Startup
{
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string loginUrl = ConfigurationManager.AppSettings["ida:LoginUrl"];
private static string authority = string.Format(loginUrl, tenantId);
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieSecure = CookieSecureOption.Always,
CookieHttpOnly = true,
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
ExpireTimeSpan = TimeSpan.FromHours(12),
SlidingExpiration = false,
CookieDomain = ".mydomain.com",
CookieName = ".AspNet.SharedCookie",
CookiePath = "/"
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
SaveTokens = true,
CookieManager = new SameSiteCookieManager(new SystemWebCookieManager()),
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username",
ValidateIssuer = false
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
private static Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
if (notification.Exception.Message.Contains("IDX21323"))
{
notification.OwinContext.Authentication.Challenge();
}
else
{
string redirect = $"/Error/Error?message={notification.Exception.Message}";
notification.Response.Redirect(redirect);
}
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
notification.HandleCodeRedemption();
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var signedInUser = new ClaimsPrincipal(notification.AuthenticationTicket.Identity);
HttpContext.Current.User = signedInUser;
var tokenStore = new TokenHelper(idClient.UserTokenCache, HttpContext.Current, signedInUser);
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code)
.WithAuthority(authority)
.ExecuteAsync();
var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
tokenStore.SaveUserDetails(userDetails);
notification.HandleCodeRedemption(null, result.IdToken);
}
catch (MsalException)
{
notification.HandleResponse();
notification.Response.Redirect($"/ErrorAccessDenied");
}
catch (Microsoft.Graph.ServiceException)
{
notification.HandleResponse();
notification.Response.Redirect($"/ErrorAccessDenied");
}
}
}
TokenHelper 有:
public class TokenHelper
{
private static readonly ReaderWriterLockSlim sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private HttpContext httpContext = null;
private string tokenCacheKey = string.Empty;
private string userCacheKey = string.Empty;
public TokenHelper(ITokenCache tokenCache, HttpContext context, ClaimsPrincipal user)
{
httpContext = context;
if (tokenCache != null)
{
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
var userId = GetUsersUniqueId(user);
tokenCacheKey = $"{userId}_TokenCache";
userCacheKey = $"{userId}_UserCache";
}
public bool HasData()
{
return (httpContext.Session[tokenCacheKey] != null &&
((byte[])httpContext.Session[tokenCacheKey]).Length > 0);
}
public void Clear()
{
sessionLock.EnterWriteLock();
try
{
httpContext.Session.Remove(tokenCacheKey);
}
finally
{
sessionLock.ExitWriteLock();
}
}
private void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
sessionLock.EnterReadLock();
try
{
// Load the cache from the session
args.TokenCache.DeserializeMsalV3((byte[])httpContext.Session[tokenCacheKey]);
}
finally
{
sessionLock.ExitReadLock();
}
}
private void AfterAccessNotification(TokenCacheNotificationArgs args)
{
if (args.HasStateChanged)
{
sessionLock.EnterWriteLock();
try
{
// Store the serialized cache in the session
httpContext.Session[tokenCacheKey] = args.TokenCache.SerializeMsalV3();
}
finally
{
sessionLock.ExitWriteLock();
}
}
}
public void SaveUserDetails(UserData user)
{
sessionLock.EnterWriteLock();
httpContext.Session[userCacheKey] = JsonConvert.SerializeObject(user);
sessionLock.ExitWriteLock();
}
public UserData GetUserDetails()
{
sessionLock.EnterReadLock();
var cachedUser = JsonConvert.DeserializeObject<UserData>((string)httpContext.Session[userCacheKey]);
sessionLock.ExitReadLock();
return cachedUser;
}
public string GetUsersUniqueId(ClaimsPrincipal user)
{
// Combine the user's object ID with their tenant ID
if (user != null)
{
var userObjectId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value ??
user.FindFirst("oid").Value;
var userTenantId = user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value ??
user.FindFirst("tid").Value;
if (!string.IsNullOrEmpty(userObjectId) && !string.IsNullOrEmpty(userTenantId))
{
return $"{userObjectId}.{userTenantId}";
}
}
return null;
}
}
AngularJS 中用来获取头像的 AccountController 是:
public class AccountController : Controller
{
[Authorize]
[HttpGet]
[AllowCrossSiteJson]
public ActionResult GetProfilePicture(string userEmail)
{
return new JsonResult { Data = new { profilePicture = GraphHelper.GetUserPicture(userEmail)}, JsonRequestBehavior = JsonRequestBehavior.AllowGet};
}
}
GraphHelper 有以下代码:
public static class GraphHelper
{
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string loginUrl = ConfigurationManager.AppSettings["ida:LoginUrl"];
private static string authority = string.Format(loginUrl, tenantId);
private static List<string> graphScopes =
new List<string>(ConfigurationManager.AppSettings["ida:AppScopes"].Split(' '));
public static async Task<UserData> GetUserDetailsAsync(string accessToken)
{
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return Task.CompletedTask;
}));
var user = await graphClient.Me.Request()
.Select(u => new {
u.DisplayName,
u.Mail,
u.UserPrincipalName,
u.Photo
})
.GetAsync();
return new UserData()
{
Avatar = null,
DisplayName = user.DisplayName,
Email = string.IsNullOrEmpty(user.Mail) ?
user.UserPrincipalName : user.Mail
};
}
public static byte[] ReadAsBytes(Stream input)
{
using (var ms = new MemoryStream())
{
input.CopyTo(ms);
return ms.ToArray();
}
}
public static string GetUserPicture(string userEmail)
{
string encodedPhoto;
try
{
var graphClient = GetAuthenticatedClient();
var requestUserPhotoFile = graphClient.Users[userEmail].Photos["64x64"].Content.Request();
var photoStream = requestUserPhotoFile.GetAsync().Result;
encodedPhoto = Convert.ToBase64String(ReadAsBytes(photoStream));
}
catch (Exception ex)
{
encodedPhoto = "ErrorGettingPhoto" + ex.ToString();
}
return encodedPhoto;
}
private static GraphServiceClient GetAuthenticatedClient()
{
return new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var tokenStore = new TokenHelper(idClient.UserTokenCache,
HttpContext.Current, ClaimsPrincipal.Current);
var userUniqueId = tokenStore.GetUsersUniqueId(ClaimsPrincipal.Current);
var account = await idClient.GetAccountAsync(userUniqueId);
// By calling this here, the token can be refreshed
// if it's expired right before the Graph call is made
var result = await idClient.AcquireTokenSilent(graphScopes, account)
.ExecuteAsync();
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", result.AccessToken);
}));
}
}
个人资料图片在本地环境中正确加载,但是,当应用程序部署时,它随机工作,有时 GraphHelper 会在行中的 TokenHelper 中抛出错误
var userObjectId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value ??
user.FindFirst("oid").Value;
如果刷新页面,它再次正常工作,并继续失败并随机工作。
Tha 应用程序正在使用 Azure Release 管道进行部署,并将其部署到两个 OnPrem 服务器中。
我尝试了多种方法,在 StartUp 类中添加了以下行,试图保留 ClaimsPrincipal.Current 属性,但有时声明加载失败。
HttpContext.Current.User = signedInUser;
任何想法将不胜感激。实施基于https://github.com/microsoftgraph/msgraph-training-aspnetmvcapp.
谢谢!