Azure active directory Azure AD Microsoft Identity Web OpenIdConnectEvents-如何在注销期间从用户令牌访问可选声明

Azure active directory Azure AD Microsoft Identity Web OpenIdConnectEvents-如何在注销期间从用户令牌访问可选声明,azure-active-directory,openid-connect,claims-based-identity,.net-core-3.1,Azure Active Directory,Openid Connect,Claims Based Identity,.net Core 3.1,将Net Core 3.1与Microsoft Identity Web和Azure广告一起使用 我正在尝试为用户登录和退出我的web应用程序项目设置一些日志记录。日志记录需要包括用户的详细信息以及他们在登录和注销期间使用的客户端端点的IP地址。然后,我通过一个用于捕获地理位置信息的扩展方法传递IP地址,该扩展方法添加到日志事件中,用于该用户身份验证 在startup.cs中,我为OpenIdConnectOptions配置了一些扩展选项,它们是: 奥托肯 OnRedirectToIdenti

将Net Core 3.1与Microsoft Identity Web和Azure广告一起使用

我正在尝试为用户登录和退出我的web应用程序项目设置一些日志记录。日志记录需要包括用户的详细信息以及他们在登录和注销期间使用的客户端端点的IP地址。然后,我通过一个用于捕获地理位置信息的扩展方法传递IP地址,该扩展方法添加到日志事件中,用于该用户身份验证

在startup.cs中,我为OpenIdConnectOptions配置了一些扩展选项,它们是:

  • 奥托肯
  • OnRedirectToIdentityProviderForSignOut
  • OnSignedOutCallbackRedirect
我创建的OpenIdeEvents类只是为了将这些方法从startup.cs文件中移出,以保持清洁

从startup.cs中摘录如下:

// Create a new instance of the class that stores the methods called
// by OpenIdConnectEvents(); i.e. when a user logs in or out the app.
// See section below :- 'services.Configure'
OpenIdEvents openIdEvents = new OpenIdEvents();

services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // The claim in the Jwt token where App roles are available.
    options.TokenValidationParameters.RoleClaimType = "roles";
    // Advanced config - capturing user events. See OpenIdEvents class.
    options.Events ??= new OpenIdConnectEvents();
    options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
    // This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
    options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
    // DO NOT DELETE - May use in the future.
    // OnSignedOutCallbackRedirect doesn't produce any claims to read for the user after they have signed out.
    options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
 });
            
“OnTokenValidatedFunc”方法如下所示:

        /// <summary>
        /// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
        /// See weblink: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.ontokenvalidated?view=aspnetcore-3.0
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task OnTokenValidatedFunc(TokenValidatedContext context)
        {
            var token = context.SecurityToken;
            var userId = token.Claims.First(claim => claim.Type == "oid").Value;
            var givenName = token.Claims.First(claim => claim.Type == "given_name").Value;
            var familyName = token.Claims.First(claim => claim.Type == "family_name").Value;
            var userName = token.Claims.First(claim => claim.Type == "preferred_username").Value;
            string ipAddress = token.Claims.First(claim => claim.Type == "ipaddr").Value;

            GeoHelper geoHelper = new GeoHelper();
            var geoInfo = await geoHelper.GetGeoInfo(ipAddress);

            string logEventCategory = "Open Id Connect";
            string logEventType = "User Login";
            string logEventSource = "WebApp_RAZOR";
            string logCountry = "";
            string logRegionName = "";
            string logCity = "";
            string logZip = "";
            string logLatitude = "";
            string logLongitude = "";
            string logIsp = "";
            string logMobile = "";
            string logUserId = userId;
            string logUserName = userName;
            string logForename = givenName;
            string logSurname = familyName;
            string logData = "User login";

            if (geoInfo != null)
            {
                logCountry = geoInfo.Country;
                logRegionName = geoInfo.RegionName;
                logCity = geoInfo.City;
                logZip = geoInfo.Zip;
                logLatitude = geoInfo.Latitude.ToString();
                logLongitude = geoInfo.Longitude.ToString();
                logIsp = geoInfo.Isp;
                logMobile = geoInfo.Mobile.ToString();
            }

            // Tested on 31/08/2020
            Log.Information(
                "{@LogEventCategory}" +
                "{@LogEventType}" +
                "{@LogEventSource}" +
                "{@LogCountry}" +
                "{@LogRegion}" +
                "{@LogCity}" +
                "{@LogZip}" +
                "{@LogLatitude}" +
                "{@LogLongitude}" +
                "{@LogIsp}" +
                "{@LogMobile}" +
                "{@LogUserId}" +
                "{@LogUsername}" +
                "{@LogForename}" +
                "{@LogSurname}" +
                "{@LogData}",
                logEventCategory,
                logEventType,
                logEventSource,
                logCountry,
                logRegionName,
                logCity,
                logZip,
                logLatitude,
                logLongitude,
                logIsp,
                logMobile,
                logUserId,
                logUserName,
                logForename,
                logSurname,
                logData);

            await Task.CompletedTask.ConfigureAwait(false);
        }
        public async Task OnRedirectToIdentityProviderForSignOutFunc(RedirectContext context)
        {
            var user = context.HttpContext.User;
            string ipAddress = user.Claims.FirstOrDefault(claim => claim.Type == "ipaddr").Value;
            var userId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
            var givenName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
            var familyName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
            var userName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
            
            // The IP Address claim is missing!
            //string ipAddress = claims.First(claim => claim.Type == "ipaddr").Value;

            await Task.CompletedTask.ConfigureAwait(false);
        }
上述方法仅给出了部分解决方案,因为我仍然需要IP地址声明,但根本不存在,但使用上述事件类型的选择无论如何都不理想

最后:

由于上下文中根本不存在任何用户声明,因此尝试订阅最后一个选项“OnSignedOutCallbackRedirect”完全是浪费时间。看起来,一旦用户点击“注销”按钮并返回到web应用程序中的“注销”页面,Microsoft就会将其转储

实际上,我需要一个解决方案,用于解决用户何时实际注销的问题,而不是在注销过程的中途,但我必须能够访问用户声明,包括在此过程中触发的上述两个事件中都不存在的IP地址

我只想简单地捕获用户的详细信息(声明)以及他们连接的客户端会话的IP地址,并在他们登录和注销web应用程序时记录这些信息。这真的太过分了吗

关于这方面的文档非常稀少,如果有人能够很好地理解MS Identity Web和OpenIDConnect事件在幕后的功能,我将非常感谢他们提供一些线索

解决方案1=能够在“OnRedirectToIdentityProviderForSignOut”期间从上下文访问IP地址声明,但当前缺少该声明

解决方案2(首选)=能够在“OnSignedOutCallbackRedirect”期间访问用户声明,但目前没有列出任何声明

先谢谢你

我需要能够在用户注销应用程序后使用OpenIdConnect生成的两个可能事件之一访问用户的声明


用户在该点注销。他/她已经不在了,所以它没有声明是有道理的,它将返回默认的空匿名用户,因为它已注销。

正如我在上面的评论中所提到的,并且考虑到Jean-Marc Prieur在上面的评论,即一旦用户完全注销,就不会出现声明,最后,我通过onRedirectToIdentityProviderForSignOutpunc方法获取了用户上下文的详细信息,然后使用一个单独的GeoHelper类来获取该用户在唱歌时(或者应该说是即将注销)所在的目的地的IP地址

是的,感谢这不是最理想的因果关系,但老实说,这不会给我带来大问题,也可能不会给其他人带来大问题。在大多数情况下,当有人注销时登录不是业务关键,更多的是了解系统使用情况。当有人到达MS弹出页面注销时,我们可能会假设99%的情况下,他们将继续并实际注销

下面是我用来实现上述场景的代码:

startup.cs类(从startup.cs代码膨胀中提取)

//创建存储调用的方法的类的新实例
//通过OpenIdConnectEvents();即当用户登录或退出应用程序时。
//请参阅下面的部分:-“服务.配置”
OpenIdEvents OpenIdEvents=新的OpenIdEvents();
//下面几行代码指示asp.net核心中间件使用Authorize属性和User.IsInrole()中“roles”声明中的数据
//看https://docs.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2了解更多信息。
配置(OpenIdConnectDefaults.AuthenticationScheme,选项=>
{
//Jwt令牌中应用程序角色可用的声明。
options.TokenValidationParameters.RoleClaimType=“角色”;
//高级配置-捕获用户事件。请参阅OpenIdeEvents类。
options.Events???=新的OpenIdConnectEvents();
options.Events.OnTokenValidated+=openIdEvents.OnTokenValidatedFunc;
//当用户重定向到MS注销页面时(在他们实际注销之前),会触发此事件
options.Events.OnRedirectToIdentityProviderForSignOut+=OpenIdeEvents.onRedirectToIdentityProviderForSignOutUNC;
//请勿删除-可能在将来使用。
//OnSignedOutCallbackRedirect不会在用户注销后为其生成任何要读取的用户声明。
options.Events.OnSignedOutCallbackRedirect+=OpenIdeEvents.OnSignedOutCallbackRedirectFunc;
});
我的地理位置自定义类:

namespace MyProject.Classes.GeoLocation
{
    /// <summary>
    /// See weblink for API documentation: https://ip-api.com/docs or https://ip-api.com/docs/api:json
    /// Note: Not free for commercial use - fee plan during development only!
    /// Sample query: http://ip-api.com/json/{ip_address}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query
    /// </summary>
    public class GeoHelper
    {
        private readonly HttpClient _httpClient;

        public GeoHelper()
        {
            _httpClient = new HttpClient()
            {
                Timeout = TimeSpan.FromSeconds(5)
            };
        }

        public async Task<GeoInfo> GetGeoInfo(string ip)
        {
            try
            {
                var response = await _httpClient.GetAsync($"http://ip-api.com/json/{ip}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query");

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();

                    return JsonConvert.DeserializeObject<GeoInfo>(json);
                }
            }
            catch (Exception)
            {
                // Do nothing, just return null.
            }

            return null;
        }
    }
}
名称空间MyProject.Classes.GeoLocation
{
/// 
///有关API文档,请参见weblink:https://ip-api.com/docs 或https://ip-api.com/docs/api:json
///注:商业用途不免费-仅在开发期间收费!
///示例查询:http://ip-api.com/json/{ip_address}?字段=状态、消息、国家、国家代码、地区、地区名称、城市、邮政编码、lat、lon、isp、手机、查询
/// 
公共类地理助手
{
私有只读HttpClientu HttpClient;
公共地理助手()
{
_httpClient=新的httpClient()
{
时间
// Create a new instance of the class that stores the methods called
// by OpenIdConnectEvents(); i.e. when a user logs in or out the app.
// See section below :- 'services.Configure'
OpenIdEvents openIdEvents = new OpenIdEvents();

// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
// See https://docs.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.

services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    // The claim in the Jwt token where App roles are available.
    options.TokenValidationParameters.RoleClaimType = "roles";
    // Advanced config - capturing user events. See OpenIdEvents class.
    options.Events ??= new OpenIdConnectEvents();
    options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
    // This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
    options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
    // DO NOT DELETE - May use in the future.
    // OnSignedOutCallbackRedirect doesn't produce any user claims to read from for the user after they have signed out.
    options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
});
namespace MyProject.Classes.GeoLocation
{
    /// <summary>
    /// See weblink for API documentation: https://ip-api.com/docs or https://ip-api.com/docs/api:json
    /// Note: Not free for commercial use - fee plan during development only!
    /// Sample query: http://ip-api.com/json/{ip_address}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query
    /// </summary>
    public class GeoHelper
    {
        private readonly HttpClient _httpClient;

        public GeoHelper()
        {
            _httpClient = new HttpClient()
            {
                Timeout = TimeSpan.FromSeconds(5)
            };
        }

        public async Task<GeoInfo> GetGeoInfo(string ip)
        {
            try
            {
                var response = await _httpClient.GetAsync($"http://ip-api.com/json/{ip}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query");

                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();

                    return JsonConvert.DeserializeObject<GeoInfo>(json);
                }
            }
            catch (Exception)
            {
                // Do nothing, just return null.
            }

            return null;
        }
    }
}
namespace MyProject.Classes.Security
{
    public class OpenIdEvents
    {
        // Create the concurrent dictionary to store the user's IP Addresss when they sign in, the value is fetched
        // from the dictionary when they sing out. given this information is not present within the contect passed through the event.
        private readonly ConcurrentDictionary<string, string> IpAddressDictionary = new ConcurrentDictionary<string, string>();

        /// <summary>
        /// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
        /// See weblink: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.ontokenvalidated?view=aspnetcore-3.0
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task OnTokenValidatedFunc(TokenValidatedContext context)
        {
            var token = context.SecurityToken;
            var userId = token.Claims.First(claim => claim.Type == "oid").Value;
            var givenName = token.Claims.First(claim => claim.Type == "given_name").Value;
            var familyName = token.Claims.First(claim => claim.Type == "family_name").Value;
            var username = token.Claims.First(claim => claim.Type == "preferred_username").Value;
            var ipAddress = token.Claims.First(claim => claim.Type == "ipaddr").Value;
            // Add the IP Address from the user's ID Token to the dictionary, we will remove
            // it from the dictionary when the user requests a sign out through OpenIDConnect. 
            IpAddressDictionary.TryAdd(userId, ipAddress);

            GeoHelper geoHelper = new GeoHelper();
            var geoInfo = await geoHelper.GetGeoInfo(ipAddress);

            string logEventCategory = "Open Id Connect";
            string logEventType = "User Sign In";
            string logEventSource = "MyProject";
            string logCountry = "";
            string logRegionName = "";
            string logCity = "";
            string logZip = "";
            string logLatitude = "";
            string logLongitude = "";
            string logIsp = "";
            string logMobile = "";
            string logUserId = userId;
            string logUserName = username;
            string logForename = givenName;
            string logSurname = familyName;
            string logData = "User with username [" + username + "] forename [" + givenName + "] surname [" + familyName + "] from IP Address [" + ipAddress + "] signed into the application [MyProject] Succesfully";

            if (geoInfo != null)
            {
                logCountry = geoInfo.Country;
                logRegionName = geoInfo.RegionName;
                logCity = geoInfo.City;
                logZip = geoInfo.Zip;
                logLatitude = geoInfo.Latitude.ToString();
                logLongitude = geoInfo.Longitude.ToString();
                logIsp = geoInfo.Isp;
                logMobile = geoInfo.Mobile.ToString();
            }

            // Tested on 31/08/2020
            Log.Information(
                "{@LogEventCategory}" +
                "{@LogEventType}" +
                "{@LogEventSource}" +
                "{@LogCountry}" +
                "{@LogRegion}" +
                "{@LogCity}" +
                "{@LogZip}" +
                "{@LogLatitude}" +
                "{@LogLongitude}" +
                "{@LogIsp}" +
                "{@LogMobile}" +
                "{@LogUserId}" +
                "{@LogUsername}" +
                "{@LogForename}" +
                "{@LogSurname}" +
                "{@LogData}",
                logEventCategory,
                logEventType,
                logEventSource,
                logCountry,
                logRegionName,
                logCity,
                logZip,
                logLatitude,
                logLongitude,
                logIsp,
                logMobile,
                logUserId,
                logUserName,
                logForename,
                logSurname,
                logData);

            await Task.CompletedTask.ConfigureAwait(false);
        }

        /// <summary>
        /// Invoked before redirecting to the identity provider to sign out.
        /// See weblink: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onredirecttoidentityproviderforsignout?view=aspnetcore-3.0&viewFallbackFrom=aspnetcore-3.1
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task OnRedirectToIdentityProviderForSignOutFunc(RedirectContext context)
        {
            var user = context.HttpContext.User;
            var userId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
            var givenName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
            var familyName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
            var username = user.Identity.Name;

            string logEventCategory = "Open Id Connect";
            string logEventType = "User Sign Out";
            string logEventSource = "MyProject";
            string logCountry = "";
            string logRegionName = "";
            string logCity = "";
            string logZip = "";
            string logLatitude = "";
            string logLongitude = "";
            string logIsp = "";
            string logMobile = "";
            string logUserId = userId;
            string logUserName = username;
            string logForename = givenName;
            string logSurname = familyName;

            IpAddressDictionary.TryRemove(userId, out string ipAddress);

            if (ipAddress != null)
            {
                // Re-fetch the geo-location details which may be different than the login session
                // given the user might have been signed in using a cell phone and move locations.
                GeoHelper geoHelper = new GeoHelper();
                var geoInfo = await geoHelper.GetGeoInfo(ipAddress);

                if (geoInfo != null)
                {
                    logCountry = geoInfo.Country;
                    logRegionName = geoInfo.RegionName;
                    logCity = geoInfo.City;
                    logZip = geoInfo.Zip;
                    logLatitude = geoInfo.Latitude.ToString();
                    logLongitude = geoInfo.Longitude.ToString();
                    logIsp = geoInfo.Isp;
                    logMobile = geoInfo.Mobile.ToString();
                }
            }

            string logData = "User with username [" + username + "] forename [" + givenName + "] surname [" + familyName + "] from IP Address [" + ipAddress + "] signed out the application [MyProject] Succesfully";

            // Tested on 31/08/2020
            Log.Information(
                "{@LogEventCategory}" +
                "{@LogEventType}" +
                "{@LogEventSource}" +
                "{@LogCountry}" +
                "{@LogRegion}" +
                "{@LogCity}" +
                "{@LogZip}" +
                "{@LogLatitude}" +
                "{@LogLongitude}" +
                "{@LogIsp}" +
                "{@LogMobile}" +
                "{@LogUserId}" +
                "{@LogUsername}" +
                "{@LogForename}" +
                "{@LogSurname}" +
                "{@LogData}",
                logEventCategory,
                logEventType,
                logEventSource,
                logCountry,
                logRegionName,
                logCity,
                logZip,
                logLatitude,
                logLongitude,
                logIsp,
                logMobile,
                logUserId,
                logUserName,
                logForename,
                logSurname,
                logData);

            await Task.CompletedTask.ConfigureAwait(false);
        }

        /// <summary>
        /// Invoked before redirecting to the SignedOutRedirectUri at the end of a remote sign-out flow.
        /// See weblink: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onsignedoutcallbackredirect?view=aspnetcore-3.0
        /// Not currently in use becuase neither the user's ID Token or claims were present. We had to use the above method instead.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task OnSignedOutCallbackRedirectFunc(RemoteSignOutContext context)
        {

            await Task.CompletedTask.ConfigureAwait(false);
        }
    }
}