0

I'm using I have authentication with AzureAD set up using OpenID Connect with an ASP.NET Core 2.2 MVC app. I want to be able to log info about the user right after a successful sign-in into the application.

In the code below, I use options.Events.OnTicketReceived event to add role claims from an external database to the Identity after they have successfully authenticated with AzureAD. However, my research tells me that the OnTicketReceived event is called right before the user is signed-in, that is, right before the local cookie is created and the user is authenticated with the app (Source: post1, post2). This would mean that the user hasn't been authenticated with the app yet, and I don't know if after the OnTicketReceived event the user is guaranteed to be authenticated.

All of this makes me wonder is it correct to log my "user signed in" message on the OnTicketReceived event or is there another way of doing it? I am also unsure if it is in Startup.cs that I should be logging this info.

Startup.cs > ConfigureServices method

public void ConfigureServices(IServiceCollection Services)
{
    //https://weblog.west-wind.com/posts/2017/dec/12/easy-configuration-binding-in-aspnet-core-revisited
    IAppSettings config = new AppSettings();
    Configuration.Bind("AppSettings", config);

    Services.AddMvc();
    Services.AddSingleton(config);
    
    //bunch of service dependencies here...

    Services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    Services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
        .AddAzureAD(options =>
        {
            options.Instance = config.AzureAD.Instance;
            options.Domain = config.AzureAD.Domain;
            options.TenantId = config.AzureAD.TenantId; //TenantID is set to "organizations"
            options.ClientId = config.AzureAD.ClientId; //the ID of the registered app in AzureAD
            options.CallbackPath = config.AzureAD.CallbackPath;
        });

    Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
    {
        options.UseTokenLifetime = false;
        options.Authority = options.Authority + "/v2.0/"; //Microsoft identity platform

        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidIssuers = config.AzureAD.Organizations; //List<string> of allowed organizations
        // https://stackoverflow.com/questions/49469979/azure-ad-b2c-user-identity-name-is-null-but-user-identity-m-instance-claims9
        // https://stackoverflow.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
        options.TokenValidationParameters.NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier";

        //Code below is to add claims during login (we add roles from db): https://stackoverflow.com/questions/51965665/adding-custom-claims-to-claimsprincipal-when-using-addazureadb2c-in-mvc-core-app
        //some discussion about this here: https://stackoverflow.com/questions/59564952/net-core-add-claim-after-azuerad-authentication
        //and here: https://stackoverflow.com/questions/52727146/net-core-2-openid-connect-authentication-and-multiple-identities
        options.Events.OnTicketReceived = context =>
        {
            string userEmail = context.Principal.FindFirstValue(ClaimTypes.Email);
            //I get an instance of UserAccess service so I can check termination status and get the roles by email.
            IUserAccess userAccess = context.HttpContext.RequestServices.GetService<IUserAccess>();
            
            bool terminated = userAccess.GetUserTerminationStatusByUserEmail(userEmail, config.ConnectionString);
            if (terminated == false)
            {
                ClaimsIdentity claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
                List<string> roles = userAccess.GetUserRolesByUserEmail(userEmail, config.ConnectionString);
                foreach (var role in roles)
                {
                    claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
                }
            }

            return Task.CompletedTask;
        };
    });

    Services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
    {
        options.AccessDeniedPath = "/UserAccess/NotAuthorized";
        options.LogoutPath = "/UserAccess/SignOut";
        options.ExpireTimeSpan = TimeSpan.FromMinutes(config.AzureAD.TimeoutInMinutes);
        options.SlidingExpiration = true;
    });
}

To sign-in and sign-out, I use the existing functions from Microsoft.AspNetCore.Authentication.AzureAD.UI NuGet package, and call them in my views like this:

<!-- to sign out -->
<a asp-area="AzureAD" asp-controller="Account" asp-action="SignOut">Sign out</a>

<!-- to sign in -->
<a asp-area="AzureAD" asp-controller="Account" asp-action="SignIn">Sign in with Microsoft</a>
Lukas
  • 1,699
  • 1
  • 16
  • 49
  • Have you tried to use `Audit Log` to get the successful sign-ins? `GET https://graph.microsoft.com/v1.0/auditLogs/signIns?&$filter=status/errorCode eq 0` – unknown Aug 06 '20 at 09:26
  • @PamelaPeng I'm actually a newbie to logging and it was set up for us to be able to use logging methods like `_logger.LogInformation("message");` using NLog. Would you mind explaining or providing a resource on what that `Audit Log` and link is and how I could implement it into the app? Thanks! – Lukas Aug 06 '20 at 13:24

1 Answers1

0

As you said, OnTicketReceived is called when clicking on the sign-in button, but what you want are the successful sign-ins.

You will see the sign-ins and Audit logs from Azure AD in the portal.

enter image description here

Sign-ins – Information about the usage of managed applications and user sign-in activities.

Audit logs - Audit logs provide system activity information about users and group management, managed applications, and directory activities.

So you don't need to add logs using NLog, the sign-in logs are automatically stored. For more details about listing sign-ins, see here.

Code Sample:

// Read application settings from appsettings.json (tenant ID, app ID, client secret, etc.)
AppSettings config = AppSettingsFile.ReadFromJsonFile();

// Initialize the client credential auth provider
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create(config.AppId)
    .WithTenantId(config.TenantId)
    .WithClientSecret(config.ClientSecret)
    .Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

// Set up the Microsoft Graph service client with client credentials
GraphServiceClient graphClient = new GraphServiceClient(authProvider);

// status/errorCode eq '0' means sign-in success
var signIns = await graphClient.AuditLogs.SignIns
    .Request()
    .Filter("status/errorCode eq '0'")
    .GetAsync();
unknown
  • 6,778
  • 1
  • 5
  • 14
  • Thank you for the response. The issue with sign-in logs being stored in AzureAD is that we would not have them in the database containing logs by NLog. It would be useful to also store the sign-ins in the database. Do you know a way to do that? Also, it's not guaranteed that we'll have permissions to view the Sign-ins page from within Azure Portal. – Lukas Aug 10 '20 at 15:02
  • @Lukas Hi, lukas. You could get sign-ins using the code, or request the [API](https://learn.microsoft.com/en-us/graph/api/signin-list?view=graph-rest-1.0&tabs=http) `GET https://graph.microsoft.com/v1.0/auditLogs/signIns`. Then store the response data to your database. There is no method to move the data into your database directly. – unknown Aug 11 '20 at 01:29