DuendeSoftware / Support

Support for Duende Software products
20 stars 0 forks source link

Login Callback of User Already Logged In #1288

Closed anjared closed 1 month ago

anjared commented 1 month ago

Which version of Duende IdentityServer are you using? 7.0.4

Which version of .NET are you using? .NET 8

Question: How would one intercept or hook into the Identity Server method that checks a user for a valid token who has already successfully logged in? Throughout our site when we navigate to another area of our app, all of which require authentication, the app transitions from page to page just fine. But we have a few areas that open a new browser window and when that happens you can see the browser first navigate to this URL:

https://stagingv2.ourwebsite.com/user/login?code=DD8ED7E4AE7EE47F9FC3F463873A6AA9C3DD822B9920B0070EA68AC0DAEBxxxx-1&scope=openid%20profile%20AN_API%20AN_ASSETS&state=95819172276241bfabc2b12c32c6ea38&session_state=2JfC0VB71-v7MVEQhk2CjHd5yUut7PXkQFBRaNvunNM.4C631B2D3764F518A79B3C24355B0CDF&iss=https%3A%2F%2Fstagingv2.ourwebsite.com%2Fauth

While it does this we get a brief page that shows "logging in" and then eventually it navigates to the page that you'd expect to see.

First of all, this process seems slow and clunky.

But my question today is if we can hook into this process so we can insert an entry into our login history table.

Any help would be appreciated.

kkdeveloper7 commented 1 month ago

Perhaps you are looking for this: https://docs.duendesoftware.com/identityserver/v7/ui/custom/

anjared commented 1 month ago

This is what we ended up doing thanks to the link above:

using MyNamespace.Data;
using MyNamespace.Models;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.ResponseHandling;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Validation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace MyNamespace.IdentityServer
{
    public class CustomAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
    {
        private readonly ApplicationDbContext _context;
        private readonly IMemoryCache _cache;

        public CustomAuthorizeInteractionResponseGenerator(
            IdentityServerOptions options,
            Duende.IdentityServer.IClock clock,
            ILogger<AuthorizeInteractionResponseGenerator> logger,
            IConsentService consent,
            IProfileService profile,
            ApplicationDbContext context,
            IMemoryCache cache
            )
            : base(options, clock, logger, consent, profile)
        {
            _context = context;
            _cache = cache;
        }

        protected override async Task<InteractionResponse> ProcessLoginAsync(ValidatedAuthorizeRequest request)
        {
            var result = await base.ProcessLoginAsync(request);

            if (!request.Subject.Identity.IsAuthenticated)
            {
                return result;
            }

            var userName = request.Subject.Identity.Name.ToLower();

            // Check if the cache entry exists - if it does, the user has already logged in today
            // It is set to expire after 1 day so non-existence means the user has not logged in today
            if (!_cache.TryGetValue($"{userName}-lastLoginDate", out DateTime lastLoginDate))
            {
                // If not, set it and log the user's login
                _cache.Set($"{userName}-lastLoginDate", DateTime.UtcNow, new MemoryCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
                });

                await LogUserLogin(userName);
            }

            return result;
        }

        private async Task LogUserLogin(string userName)
        {
            var user = await _context.Users.FirstOrDefaultAsync(x => x.UserName == userName);
            if (user != null)
            {
                _context.UserLoginHistories.Add(new UserLoginHistory { User = user });
                await _context.SaveChangesAsync();
            }
        }
    }
}

You also need to register this new custom generator in the Startup.cs file:

var builder = services.AddIdentityServer(options =>
{
    Console.WriteLine("Setting identity server events options.");

    options.Events.RaiseErrorEvents = true;
    options.Events.RaiseInformationEvents = true;
    options.Events.RaiseFailureEvents = true;
    options.Events.RaiseSuccessEvents = true;
    // Add any other options you might need here.  
})
.AddAspNetIdentity<User>()
// Other things like config stores, operationalstores, etc.  
.AddAuthorizeInteractionResponseGenerator<CustomAuthorizeInteractionResponseGenerator>();