Base solution for your next web application
Open Closed

How to add custom data to AbpUserLoginAttempts table #8367


User avatar
0
timmackey created

I would like to add a string column to the AbpUserLoginAttempts table to include my own custom data (client screen info). There doesn't seem to be an easy way to do this, unlike with the User table, which has a section "Can add application specific user properties here" (which I have used successfully.)

Aside from hand-modifying "myProjectDbContextModelSnapshot.cs" (which I am loath to do), I haven't found another solution. Also not found:

  • how BrowserInfo, ClientIpAddress, etc. is added to the table on the server.
  • the code on the client where BrowserInfo, ClientIpAddress, etc are sent to the server.

If it isn't possible or practical to modify AbpUserLoginAttempts I could create another table for my custom data, but would still need a way to reference the correct AbpUserLoginAttempts Id.

MyProject: Angular/Core


15 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team

    Have you read this document?

    https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Extending-Existing-Entities-Core-Angular#derive-from-edition-entity

  • User Avatar
    0
    timmackey created

    I read the document.

    How would I identify the logged-in user's record in AbpUserLoginAtttempts table? How would I determine the Id to select for?

  • User Avatar
    0
    maliming created
    Support Team

    EF Core will handle it automatically, you just need to use the repository normally.

  • User Avatar
    0
    timmackey created

    To use any table "normally" one must know which Id (row) to update. Which row should my code update for the logged-in user?

    I have been unable to find any code that writes to this table. Could you please direct me to a file which does this?

    The only examples I can find are retrieving all AbpUserLoginAtttempts in bulk form.

  • User Avatar
    0
    ryancyq created
    Support Team

    Hi, the UserLoginAttempts are created when logging in via AbpSignManager.

    See https://github.com/aspnetboilerplate/aspnetboilerplate/blob/c0604b9b1347a3b9581bf97b4cae22db5b6bab1b/src/Abp.ZeroCore/Authorization/AbpLoginManager.cs#L232

    Not sure how would you update the LoginAttempts but there isnt any Attempt Id for you to reference outside of AbpSignInManager.

    You probably will need to retrieve the last login attempt to update it (but this probably won't be reliable for update operation)

  • User Avatar
    0
    timmackey created

    @ryancyq - Thank you for your comment and directing me to the source code :)

    It seems my only choices are:

    1. Download ABP source code and modify it to meet my requirement. This choice would be impractible as maintainence would be a nightmare.
    2. Duplicate all this information in a new table of my own design, which would be the same columns as AbpUserLoginAtttempts plus my custom columns, and create another similar client UI. Grossly inefficient, but doable.
    3. Implore ANZ to design a solution to my problem (custom data). Here are couple of suggestions: A. Make AbpUserLoginAtttempts Id available (via UserManager?) and extend the class using this method recommended by @maliming. B. Add a UserDefined column of type string to AbpUserLoginAtttempts table, and modify LogInManager.LoginAsync method with an additional optional argument (i.e. string OptionalUserDefined = string.Empty). This technique would allow a developer to define literally any structure encoded as a JSON string, inhibit update/create access to the table, and not affect existing implementations.
  • User Avatar
    0
    ryancyq created
    Support Team

    Hi, overriding SaveLoginAttempt logic isn't as difficult and hard to maintain, ANZ project already SignInManager create for easy implementation of custom logic.

    See https://github.com/aspnetzero/aspnet-zero-core/blob/dev/aspnet-core/src/MyCompanyName.AbpZeroTemplate.Core/Identity/SignInManager.cs , you just need to override a single method (SaveLoginAttempt) in that file.

  • User Avatar
    0
    timmackey created

    @ryancyq - I don't understand how to do what you're suggesing. Specifically, how is a user-defined string added to the correct row in the table? Could you please provide more detailed information? To summarize, I want to add a custom string that originates on the client (at login) in a new column in AbpUserLoginAtttempts table.

  • User Avatar
    0
    timmackey created

    @support - Thank you for the code sample via email.

    I have successfully written the constant string "test..." to my new column ClientInfo in AbpUserLoginAttempts table.

    protected override async Task SaveLoginAttemptAsync(AbpLoginResult<Tenant, User> loginResult, string tenancyName, string userNameOrEmailAddress)
    {
    	using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress))
    	{
    		var tenantId = loginResult.Tenant != null ? loginResult.Tenant.Id : (int?)null;
    		using (UnitOfWorkManager.Current.SetTenantId(tenantId))
    		{
    			var loginAttempt = new TtmUserLoginAttempt
    			{
    				TenantId = tenantId,
    				TenancyName = tenancyName,
    				UserId = loginResult.User != null ? loginResult.User.Id : (long?)null,
    				UserNameOrEmailAddress = userNameOrEmailAddress,
    				Result = loginResult.Result,
    				BrowserInfo = ClientInfoProvider.BrowserInfo,
    				ClientIpAddress = ClientInfoProvider.ClientIpAddress,
    				ClientName = ClientInfoProvider.ComputerName,
    				ClientInfo = "test..."
    			};
    			await _myUserLoginAttemptRepository.InsertAsync(loginAttempt);
    			await UnitOfWorkManager.Current.SaveChangesAsync();
    			await uow.CompleteAsync();
    		}
    	}
    }
    

    The only question remaining is how to replace the constant "test..." string with my client data? Getting client data to the server via TokenAuthController.Authenticate(...) is easily done. The signature of SaveLoginAttemptAsync can't be changed. ANZ app doesn't call SaveLoginAttemptAsync directly, so adding a parameter would be useless anyway. The my string can't be piggybacked with an existing parameter. The LogInManager class is instantiated in Abp library, not by ANZ app. So, no adding parameters there either.

    Sorry if this sounds like a dumb question that any decent C# programmer could answer. I don't see any mechanism that would allow the string presented during authentication to be available in SaveLoginAttemptAsync.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    You can add it here https://github.com/aspnetzero/aspnet-zero-core/blob/dev/aspnet-core/src/MyCompanyName.AbpZeroTemplate.Web.Core/Controllers/TokenAuthController.cs#L626 to the loginResult.Identity claim list. Then, you can get it back in SaveLoginAttemptAsync.

  • User Avatar
    0
    timmackey created

    @ismcagdas -

    Adding my ClientInfo string to the loginResult.Identity claim list is too late in the process, as LogInManager.SaveLoginAttemptAsync is called before logInManager.LoginAsync returns loginResult.

  • User Avatar
    0
    aaron created
    Support Team

    In TokenAuthController.cs, pass the custom data in a subclass of UserLoginInfo to _logInManager.LoginAsync. In LogInManager.cs, override LoginAsync to forward the custom data in a subclass of AbpLoginResult<Tenant, User> to SaveLoginAttemptAsync.

  • User Avatar
    0
    timmackey created

    I was not able to find a LoginAsync method to override so used the closest match LoginAsyncInternal.

    This is what I have so far:

    LoginManager.cs

    using Abp.Authorization;
    using Abp.Authorization.Users;
    using Abp.Configuration;
    using Abp.Configuration.Startup;
    using Abp.Dependency;
    using Abp.Domain.Repositories;
    using Abp.Domain.Uow;
    using Abp.Zero.Configuration;
    using Microsoft.AspNetCore.Identity;
    using ngTTM.Authorization.Roles;
    using ngTTM.Authorization.Users;
    using ngTTM.MultiTenancy;
    using System.Threading.Tasks;
    using System.Transactions;
    
    namespace ngTTM.Authorization
    {
        public class LogInManager : AbpLogInManager<Tenant, Role, User>
        {
            private IRepository<TtmUserLoginAttempt, long> _ttmUserLoginAttemptRepository;
            private string _clientInfo;
    
            public LogInManager(
                UserManager userManager,
                IMultiTenancyConfig multiTenancyConfig,
                IRepository<Tenant> tenantRepository,
                IUnitOfWorkManager unitOfWorkManager,
                ISettingManager settingManager,
                IRepository<UserLoginAttempt, long> userLoginAttemptRepository,
                IUserManagementConfig userManagementConfig,
                IIocResolver iocResolver,
                RoleManager roleManager,
                IPasswordHasher<User> passwordHasher,
                UserClaimsPrincipalFactory claimsPrincipalFactory,
                IRepository<TtmUserLoginAttempt, long> ttmUserLoginAttemptRepository)
                : base(
                      userManager,
                      multiTenancyConfig,
                      tenantRepository,
                      unitOfWorkManager,
                      settingManager,
                      userLoginAttemptRepository,
                      userManagementConfig,
                      iocResolver,
                      passwordHasher,
                      roleManager,
                      claimsPrincipalFactory)
            {
                _ttmUserLoginAttemptRepository = ttmUserLoginAttemptRepository;
            }
    
            protected override Task<AbpLoginResult<Tenant, User>> LoginAsyncInternal(UserLoginInfo login, string tenancyName)
            {
                TtmUserLoginInfo ttmUserLoginInfo = login as TtmUserLoginInfo;
                _clientInfo = ttmUserLoginInfo.ClientInfo;
                return base.LoginAsyncInternal(login, tenancyName);
            }
    
            protected override async Task SaveLoginAttemptAsync(AbpLoginResult<Tenant, User> loginResult, string tenancyName, string userNameOrEmailAddress)
            {
                using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress))
                {
                    var tenantId = loginResult.Tenant != null ? loginResult.Tenant.Id : (int?)null;
    
                    using (UnitOfWorkManager.Current.SetTenantId(tenantId))
                    {
                        var loginAttempt = new TtmUserLoginAttempt
                        {
                            TenantId = tenantId,
                            TenancyName = tenancyName,
                            UserId = loginResult.User != null ? loginResult.User.Id : (long?)null,
                            UserNameOrEmailAddress = userNameOrEmailAddress,
                            Result = loginResult.Result,
                            BrowserInfo = ClientInfoProvider.BrowserInfo,
                            ClientIpAddress = ClientInfoProvider.ClientIpAddress,
                            ClientName = ClientInfoProvider.ComputerName,
                            ClientInfo = _clientInfo
                        };
                        await _ttmUserLoginAttemptRepository.InsertAsync(loginAttempt);
                        await UnitOfWorkManager.Current.SaveChangesAsync();
                        await uow.CompleteAsync();
                    }
                }
            }
    
        }
    }
    

    TtmUserLoginInfo.cs

    using Microsoft.AspNetCore.Identity;
    
    namespace ngTTM.Authorization.Users
    {
        public class TtmUserLoginInfo : UserLoginInfo
        {
            public string ClientInfo { get; set; }
    
            public TtmUserLoginInfo(string loginProvider, string providerKey, string displayName, string clientInfo): base(loginProvider, providerKey, displayName)
            {
                ClientInfo = clientInfo;
            }
    
        }
    }
    

    TokenAuthController.cs

            private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName, string clientInfo)
            {
                // the old way which works without clientInfo
                //AbpLoginResult<Tenant, User> loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);
    
                // the new way as recommended by @aaron
                TtmUserLoginInfo ttmUserLoginInfo = new TtmUserLoginInfo("", "", "", clientInfo);
                AbpLoginResult<Tenant, User> loginResult = await _logInManager.LoginAsync(ttmUserLoginInfo, tenancyName);
    
                switch (loginResult.Result)
                {
                    case AbpLoginResultType.Success:
                        return loginResult;
                    default:
                        throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt(loginResult.Result, usernameOrEmailAddress, tenancyName);
                }
            }
    

    While I am able to pass clientInfo into LogInManager, the login fails (no surprise) since I don't know what values to use for loginProvider, providerKey, and displayName for UserLoginInfo.

    What are the argument values to use to login to my site (and localhost)?

  • User Avatar
    0
    aaron created
    Support Team

    On second thought, it may be more appropriate to overload LoginAsync and SaveLoginAttemptAsync.

    LogInManager.cs

    [UnitOfWork]
    public virtual async Task<AbpLoginResult<Tenant, User>> LoginAsync(ClientInfo clientInfo, string userNameOrEmailAddress, string plainPassword, string tenancyName = null, bool shouldLockout = true)
    {
        var result = await LoginAsyncInternal(userNameOrEmailAddress, plainPassword, tenancyName, shouldLockout);
        await SaveLoginAttemptAsync(clientInfo, result, tenancyName, userNameOrEmailAddress);
        return result;
    }
    
    protected virtual async Task SaveLoginAttemptAsync(ClientInfo clientInfo, AbpLoginResult<TTenant, TUser> loginResult, string tenancyName, string userNameOrEmailAddress)
    {
        var innerClientInfo = clientInfo.InnerClientInfo;
    
        ...
    }
    

    ClientInfo.cs

    public class ClientInfo
    {
        public string InnerClientInfo { get; set; }
    }
    

    TokenAuthController.cs

    private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName, string clientInfo)
    {
        // var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName);
        var outerClientInfo = new ClientInfo { InnerClientInfo = clientInfo };
        var loginResult = await _logInManager.LoginAsync(outerClientInfo, usernameOrEmailAddress, password, tenancyName);
    
        ...
    }
    
  • User Avatar
    1
    timmackey created

    That solution works! Thank you!

    I had to make a few minor changes to get the code to build. Here is the complete solution for anyone who might want to do something similar.

    .Core/TtmUserLoginAttempts.cs (perform Add-Migration, Update-Database)

    using Abp.Authorization.Users;
    
    namespace ngTTM.Authorization.Users
    {
        public class TtmUserLoginAttempt : UserLoginAttempt
        {
            public string ClientInfo { get; set; }
        }
    }
    

    .EntityFrameworkCore/EntityFrameworkCore/ntTTMDbContext.cs

            public virtual DbSet<TtmUserLoginAttempt> TtmUserLoginAttempts { get; set; }
    

    .Web.Core/Models/TokenAuth/AuthenticateModel.cs

            public string ClientInfo { get; set; }
    

    .Web.Core/Controllers/TokenAuthController.cs

    private async Task<AbpLoginResult<Tenant, User>> GetLoginResultAsync(string usernameOrEmailAddress,
                                                                         string password,
                                                                         string tenancyName,
                                                                         string clientInfo)
    {
        var loginResult = await _logInManager.LoginAsync(clientInfo,
                                                         usernameOrEmailAddress,
                                                         password,tenancyName);
    
        switch (loginResult.Result)
        {
            case AbpLoginResultType.Success:
                return loginResult;
            default:
                throw _abpLoginResultTypeHelper
                    .CreateExceptionForFailedLoginAttempt(loginResult.Result,
                                                          usernameOrEmailAddress,
                                                          tenancyName);
        }
    }
    

    .Application/Authorization/LogInManager.cs

    using Abp.Authorization;
    using Abp.Authorization.Users;
    using Abp.Configuration;
    using Abp.Configuration.Startup;
    using Abp.Dependency;
    using Abp.Domain.Repositories;
    using Abp.Domain.Uow;
    using Abp.Zero.Configuration;
    using Microsoft.AspNetCore.Identity;
    using ngTTM.Authorization.Roles;
    using ngTTM.Authorization.Users;
    using ngTTM.MultiTenancy;
    using System.Threading.Tasks;
    using System.Transactions;
    
    namespace ngTTM.Authorization
    {
        public class LogInManager : AbpLogInManager<Tenant, Role, User>
        {
            private IRepository<TtmUserLoginAttempt, long> _ttmUserLoginAttemptRepository;
    
            public LogInManager(
                UserManager userManager,
                IMultiTenancyConfig multiTenancyConfig,
                IRepository<Tenant> tenantRepository,
                IUnitOfWorkManager unitOfWorkManager,
                ISettingManager settingManager,
                IRepository<UserLoginAttempt, long> userLoginAttemptRepository,
                IUserManagementConfig userManagementConfig,
                IIocResolver iocResolver,
                RoleManager roleManager,
                IPasswordHasher<User> passwordHasher,
                UserClaimsPrincipalFactory claimsPrincipalFactory,
                IRepository<TtmUserLoginAttempt, long> ttmUserLoginAttemptRepository)
                : base(
                      userManager,
                      multiTenancyConfig,
                      tenantRepository,
                      unitOfWorkManager,
                      settingManager,
                      userLoginAttemptRepository,
                      userManagementConfig,
                      iocResolver,
                      passwordHasher,
                      roleManager,
                      claimsPrincipalFactory)
            {
                _ttmUserLoginAttemptRepository = ttmUserLoginAttemptRepository;
            }
    
            [UnitOfWork]
            public virtual async Task<AbpLoginResult<Tenant, User>> LoginAsync(string clientInfo,
                                                                                   string userNameOrEmailAddress,
                                                                                   string plainPassword,
                                                                                   string tenancyName = null,
                                                                                     bool shouldLockout = true)
            {
                var result = await LoginAsyncInternal(userNameOrEmailAddress, plainPassword, tenancyName, shouldLockout);
                await SaveLoginAttemptAsync(clientInfo, result, tenancyName, userNameOrEmailAddress);
                return result;
            }
    
            protected virtual async Task SaveLoginAttemptAsync(string clientInfo,
                                             AbpLoginResult<Tenant, User> loginResult,
                                                                   string tenancyName,
                                                                   string userNameOrEmailAddress)
            {
                using (var uow = UnitOfWorkManager.Begin(TransactionScopeOption.Suppress))
                {
                    var tenantId = loginResult.Tenant != null ? loginResult.Tenant.Id : (int?)null;
    
                    using (UnitOfWorkManager.Current.SetTenantId(tenantId))
                    {
                        var loginAttempt = new TtmUserLoginAttempt
                        {
                            TenantId = tenantId,
                            TenancyName = tenancyName,
                            UserId = loginResult.User != null ? loginResult.User.Id : (long?)null,
                            UserNameOrEmailAddress = userNameOrEmailAddress,
                            Result = loginResult.Result,
                            BrowserInfo = ClientInfoProvider.BrowserInfo,
                            ClientIpAddress = ClientInfoProvider.ClientIpAddress,
                            ClientName = ClientInfoProvider.ComputerName,
                            ClientInfo = clientInfo
                        };
                        await _ttmUserLoginAttemptRepository.InsertAsync(loginAttempt);
                        await UnitOfWorkManager.Current.SaveChangesAsync();
                        await uow.CompleteAsync();
                    }
                }
            }
        }
    }
    

    /src/account/login/login.service.ts

        authenticate(finallyCallback?: () => void, redirectUrl?: string, captchaResponse?: string): void {
            finallyCallback = finallyCallback || (() => {
                this.spinnerService.hide();
            });
    
            // We may switch to localStorage instead of cookies
            this.authenticateModel.twoFactorRememberClientToken = this._utilsService.getCookieValue(LoginService.twoFactorRememberClientTokenName);
            this.authenticateModel.singleSignIn = UrlHelper.getSingleSignIn();
            this.authenticateModel.returnUrl = UrlHelper.getReturnUrl();
            this.authenticateModel.captchaResponse = captchaResponse;
            this.authenticateModel.clientInfo = this.getClientInfo();
    
            this._tokenAuthService
                .authenticate(this.authenticateModel)
                .subscribe({
                    next: (result: AuthenticateResultModel) => {
                        this.processAuthenticateResult(result, redirectUrl);
                        finallyCallback();
                    },
                    error: (err: any) => {
                        finallyCallback();
                    }
                });
        }