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)
-
0
Have you read this document?
https://docs.aspnetzero.com/en/aspnet-core-angular/latest/Extending-Existing-Entities-Core-Angular#derive-from-edition-entity
-
0
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?
-
0
EF Core will handle it automatically, you just need to use the repository normally.
-
0
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.
-
0
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)
-
0
@ryancyq - Thank you for your comment and directing me to the source code :)
It seems my only choices are:
- Download ABP source code and modify it to meet my requirement. This choice would be impractible as maintainence would be a nightmare.
- 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.
- 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.
-
0
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.
-
0
@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.
-
0
@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.
-
0
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 inSaveLoginAttemptAsync
. -
0
@ismcagdas -
Adding my ClientInfo string to the
loginResult.Identity
claim list is too late in the process, asLogInManager.SaveLoginAttemptAsync
is called beforelogInManager.LoginAsync
returnsloginResult
. -
0
In TokenAuthController.cs, pass the custom data in a subclass of
UserLoginInfo
to_logInManager.LoginAsync
. In LogInManager.cs, overrideLoginAsync
to forward the custom data in a subclass ofAbpLoginResult<Tenant, User>
toSaveLoginAttemptAsync
. -
0
I was not able to find a
LoginAsync
method to override so used the closest matchLoginAsyncInternal
.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)?
-
0
On second thought, it may be more appropriate to overload
LoginAsync
andSaveLoginAttemptAsync
.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); ... }
-
1
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(); } }); }