Azure AD Microsoft Identity Web OpenIdConnectEvents – How to access optional claims from the user token during sign out

Using Net Core 3.1 with Microsoft Identity Web and Azure AD.

I'm trying to setup some logging for when a user signs in and out of my web app project. The logging needs to include details of the user as well as the IP Address of the client endpoint they used during sign in and sign out. I then pass the IP Address through an extension method for capturing Geo Location info that is added to the log event for that user authentication.

In startup.cs I have configured some extended options for the OpenIdConnectOptions, they are:

  • OnTokenValidated
  • OnRedirectToIdentityProviderForSignOut
  • OnSignedOutCallbackRedirect

The OpenIdEvents class I created is just simply to move away the methods from the startup.cs file for cleanliness.

Extract from startup.cs below:

// 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;
 });
            

So far I have found a solution to capture the required claims of the user for when they sign in, the 'TokenValidatedContext' passed to the first method 'OnTokenValidatedFunc' contains details of the security token which in itself shows the optional claims that I had configured including the IP Address (referred to as "ipaddr")

Some of these optional claims were configured in the App manifest file in Azure, they are present in the security token in this first method so pretty sure Azure is setup correctly.

Extract from Azure App Manifest File:

"optionalClaims": {
        "idToken": [
            {
                "name": "family_name",
                "source": null,
                "essential": false,
                "additionalProperties": []
            },
            {
                "name": "given_name",
                "source": null,
                "essential": false,
                "additionalProperties": []
            },
            {
                "name": "ipaddr",
                "source": null,
                "essential": false,
                "additionalProperties": []
            }
        ],
        "accessToken": [],
        "saml2Token": []
    },

'OnTokenValidatedFunc' method shown below:

        /// <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);
        }

See Debug shots below:

enter image description here

When expanding the claims, you can see the claim for "ipaddr" is shown:

enter image description here

MY ISSUE:

The other event types fired from OpenIdConnectEvents for when the user signs out, does not function in the same way and this is where I am stuck!

There are two different event types I have tried testing with:

  • OnRedirectToIdentityProviderForSignOut
  • OnSignedOutCallbackRedirect

Each one is fired at a slightly different point during the user sign out process i.e. the 'OnRedirectToIdentityProviderForSignOutFunc' is fired when the user is being re-directed to the Microsoft Sign Out page, just before they actually hit the button and sign out.

This is not an ideal event type to work with given the user could abort signing out of the application and the log generated would not reflect this, however I have so far found that I could at least access most of the claims of the user, BUT the "ipaddr" claim is not listed and I simply don't know why or how to get it.

When I look at the Debug info I find the security token is not shown at all and the only way to access the user claims was to read another part of the context by navigating to context.HttpContext.User.Claims

Debug Screenshot:

enter image description here

The method for this shown below:

        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);
        }

The above method only gives a partial solution given I still need the IP Address claim but is not present at all, but the choice in using this event type as explained above is not ideal anyway.

AND FINALLY:

Trying to subscribe to the final option 'OnSignedOutCallbackRedirect' has been a complete waste of time so far given none of the user claims are present at all in the context. It seems that Microsoft dumps them once the user has hit the Sign out button and returned back to the 'Signed Out' page in the web app.

Really I want a solution for when the user has actually signed out, not half way through the process of signing out, BUT I must be able access the user claims including the IP Address which is not present in either of the above two events fired during this process.

All I want is to simply capture the details (claims) of the user and the IP Address of the client session they are connecting from and log this when they sign in and sign out of the web application. Is this really too much to ask!

Documentation on this is very sparse, I would much appreciate some clues from anyone out there who understands well how MS Identity Web and OpenIDConnect Events function behind the scenes.

Solution 1 = Being able to access the IP Address claim from the context during 'OnRedirectToIdentityProviderForSignOut' but it is currently missing...

Solution 2 (Preferred) = Being able to access the user claims during 'OnSignedOutCallbackRedirect' but currently none of them are listed at all.

Thanks in advance...



Read more here: https://stackoverflow.com/questions/63675534/azure-ad-microsoft-identity-web-openidconnectevents-how-to-access-optional-cla

Content Attribution

This content was originally published by OJB1 at Recent Questions - Stack Overflow, and is syndicated here via their RSS feed. You can read the original post over there.

%d bloggers like this: