ASP.net MVC 应用程序使用 Azure OpenID Connect 在本地工作正常但无法从 ClaimsPrincipal 读取 oid。当前正在刷新页面

问题描述 投票:0回答:0

提前感谢您提供的任何帮助。我在 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.

谢谢!

asp.net-mvc azure-active-directory microsoft-graph-api openid
© www.soinside.com 2019 - 2024. All rights reserved.