Get users last accessed time - c#

I am using IdentityServer 4 (with sql storage) for my asp.net core API. I would like to add a "Last Accessed" field somewhere in the database that can be used to order one of my user types in a user search endpoint. Ideally this would show the last time they performed an action in the system.
If I do this in the login method it won't be accurate as it will only update when a token is initially generated. I could add a method to every endpoint that the user accessed but this doesn't feel like the right solution as it would be repeating myself.
What is the correct place and method for maintaining a database field that records the last time a user accessed the system?

For recording the user active time, you could try Middleware which will be called for every request.
Here are steps.
1.Add field to ApplicationUser which is store the last accessed time
public class ApplicationUser : IdentityUser
{
public DateTime LastAccessed { get; set; }
}
2.run command add-migration LastAccessed and update-database
3.Add middleware to update the last accessed time based on the user name
app.UseAuthentication();
app.Use(async (context, next) => {
await next.Invoke();
//handle response
//you may also need to check the request path to check whether it requests image
if (context.User.Identity.IsAuthenticated)
{
var userName = context.User.Identity.Name;
//retrieve uer by userName
using (var dbContext = context.RequestServices.GetRequiredService<ApplicationDbContext>())
{
var user = dbContext.ApplicationUser.Where(u => u.UserName == userName).FirstOrDefault();
user.LastAccessed = DateTime.Now;
dbContext.Update(user);
dbContext.SaveChanges();
}
}
});

I added an answer for a similar question which I did with a centralized solution in the identity server project (using IEventSink). We have 30+ apis and I found it better to do the solution in only one place instead of doing it in each and every api. So i added this answer here just in case anybody came across and have the same requirements as mine. This is my answer

Related

Get data from a table after having closed the session

In this moment I´m try to get a List of users and checks if the user is in the BD or not
I´m using Web API Net 6 and Sql Server
This is the code
[HttpPost("login")]
public async Task<ActionResult<string>> Login(LoginDto request)
{
//In this line I´m try to get the list of users (this bottom line doesn't work)
await _context.Users.ToListAsync();
if(user.UserName != request.UserName)
{
return BadRequest("User Not Found");
}
// ...
Here the problem is that the program has been running for 1 time until it works normally but when I end the session and come back again there is an application on the 2nd time it can no longer find the user in the database. My idea then is to add that line of code that just doesn't work (I don't know if it's due to await or if it's wrong to get through ListAsync() or if it's due to the user inside the if not being connected with the _context of the database )
By the way, that user is static having declared it like this
-> public static User user = new User();
Can anyone help me with this problem or tell me better solutions on how to get data from a table
If you just want to search your Users table for a user record with the name passed in the LoginDTO instance, then you just ask it to the database context to search for that name.
var userInDb = await _context.Users.FirstOrDefaultAsync(x => x.UserName == request.UserName);
if(userInDb == null)
... not found ....
But let me understand better your problem. If you are implementing your custom authorization and verification infrastructure for users, then think twice becase is not as simple as it looks. (For example, how do you store passwords in that table?) There is a dedicated library for that from Microsoft and is called ASP.NET Identity

ASP.Net Core WebAPI Authorization Policy for User or Admin

I have a controller that returns data about users. I want to set the authorization such that an admin can access this controller and retrieve data for any user, and a non-admin user can access the controller and retrieve data for themselves.
I've ruled out using [Authorize (Roles = "Admin")] because this means users can't get their own data. So I've inserted the following logic into the controller action:
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Name).Value;
var roles = _httpContextAccessor.HttpContext.User.FindAll(ClaimTypes.Role);
var query = roles.Select(r => r.Value).Contains("Admin");
Customer customer =await _context.Customers.FindAsync(id);
if (!(customer.EmailAddress == userId || query))
return Unauthorized();
This is roughly equivalent to this Stack Overflow answer, but for ASP.Net Core rather than MVC.
My question is, is there a way to do this with an Authorization Policy? Adding the RequireRole check is straightforward and covered in the MS Documentation as well as countless blogs, but I couldn't find or figure out a way to use a policy to check that the data the user is trying to access is their own.
I'm sure this isn't an uncommon requirement, is there a way to do this, or is what I'm currently doing OK? The only other approach I could think of was to have two separate endpoints, but both options seem inelegant.
The policy is for authorization , but either Admin or A normal user can access the controller , they are all authorized .
That is your custom logic to determine which data should be returned , that is nothing related to authorization . If you insist on using policy , you can put the logic to handler but that is nothing change when logic is in controller :
public class CustomerHandler : AuthorizationHandler<CustomerRequirement>
{
IHttpContextAccessor _httpContextAccessor = null;
public CustomerHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
CustomerRequirement requirement)
{
HttpContext httpContext = _httpContextAccessor.HttpContext;
//your logic
httpContext.Items["message"] = "ownData";
context.Succeed(requirement);
return Task.CompletedTask;
}
}
And read in controller so that you can know whether read his own data or all users' data :
var message = HttpContext.Items["message"];
In my option ,set two endpoints/function in your web api , one for admin , one for user is the clean way . In addition , that is your client app's responsibility to determine that current user wants to return his own data or all user's data . That seems not quite correct to send request to web api and let api to determine by logic . Webapi should include the clean functions/endpoint to map each request from client .

Avoid duplicate POSTs with .NET Core

I'm using POST in a .NET Core REST API to insert data on database.
In my client application, when the user clicks a button, I disable this button. But sometimes, because some reason, the click of the button may be faster than the function of the disable button. This way, the user can double click on the button and the POST will be sent twice, inserting the data twice.
To do POST I'm using axios on the client side. But how can I avoid this on the server side?
Handling concurrency with inserts is hard, frankly. Things like updates and deletes are relatively trivial as you can use concurrency tokens. When doing an update for instance, a WHERE clause is added to check the row that is about to be updated concurrency token value. If it doesn't match, that means it was updated since the data was last queried, and you can then implement some sort of recovery stategy.
Inserts don't work the same way because there's obviously nothing there yet to compare to. Your best bet is a somewhat convoluted strategy of assigning some id to a particular insertion. This will have to be persisted on a column in your table, and that column will need to be unique. When you display the form, you set a hidden input with a unique-ish value, such as Guid.NewGuid(). This will then be posted back when the user submits. This then gets added to your entity, and when you save it will be set on the row that's created.
Now let's say the user double-click the submit button firing off two nearly simultaneous requests. Because the same form data is being submit for both requests, the same id is present in both submissions. The one that makes it first ends up saving the record to the database, while the next will end up throwing an exception. Since the column the id is being saved to is unique, and the same id was sent for both requests, the second one will fail to save. At this point, you can catch the exception and recover some how.
My personal recommendation is to make it seamless to the user. When you hit the catch, you query the row that was actually inserted with that id, and return that id/data instead. For example, let's say this was for a checkout page and you were creating orders. You're likely going to redirect the user to an order confirmation page after completion. So, on the request that fails, you look up the order that was actually created, and then you just redirect to the order confirmation page immediately with that order number/id. As far as the user is concerned, they just went to directly to the confirmation page, and your app ended up only inserting one order. Seamless.
I have had this scenario some time ago. I created an Action Filter for it, which is using an Anti Fogery Token:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class PreventDoublePostAttribute : ActionFilterAttribute
{
private const string TokenSessionName = "LastProcessedToken";
public override void OnActionExecuting(ActionExecutingContext context)
{
var antiforgeryOptions = context.HttpContext.RequestServices.GetOption<AntiforgeryOptions>();
var tokenFormName = antiforgeryOptions.FormFieldName;
if (!context.HttpContext.Request.Form.ContainsKey(tokenFormName))
{
return;
}
var currentToken = context.HttpContext.Request.Form[tokenFormName].ToString();
var lastToken = context.HttpContext.Session.GetString(TokenSessionName);
if (lastToken == currentToken)
{
context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
return;
}
context.HttpContext.Session.SetString(TokenSessionName, currentToken);
}
}
Simple use it on your method:
[HttpPost]
[PreventDoublePost]
public async Task<IActionResult> Edit(EditViewModel model)
{
if (!ModelState.IsValid)
{
//PreventDoublePost Attribute makes ModelState invalid
}
throw new NotImplementedException();
}
Make sure, you generate the Anti Fogery Token, see the documentation on how it works for Javascript or Angular.
If you use a relational database the simplest way is to add unique constraint to the table(s) where data is populated. If it's impossible or database isn't relational and you have single server instance you can use synchronization inside the application code, that is keep single instance of an entity to be populated into db and modify this instance quintessentially by using synchronization primitives like lock, etc. But this approach has significant drawback - it doesn't work if there multiple instance of your web application (on different servers for example). Another approach you can apply is using versioning approach - that is you can keep version of modification along with your data and do read before write into a database (in order to increment version) with turned on optimistic locking on the db side (most of dbs support this).
This answer inspired by #Christian Gollhardt answer
First you need to enable session in your stratup.cs add
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = Context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMemoryCache();
services.AddSession(options => {
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromMinutes(10);
options.Cookie.HttpOnly = true;
// Make the session cookie essential
options.Cookie.IsEssential = true;
});
and then
app.UseSession();
then your class
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class PreventDoublePostAttribute : ActionFilterAttribute
{
private const string UniqFormuId = "LastProcessedToken";
public override async void OnActionExecuting(ActionExecutingContext context)
{
IAntiforgery antiforgery = (IAntiforgery)context.HttpContext.RequestServices.GetService(typeof(IAntiforgery));
AntiforgeryTokenSet tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
if (!context.HttpContext.Request.Form.ContainsKey(tokens.FormFieldName))
{
return;
}
var currentFormId = context.HttpContext.Request.Form[tokens.FormFieldName].ToString();
var lastToken = "" + context.HttpContext.Session.GetString(UniqFormuId);
if (lastToken.Equals(currentFormId))
{
context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
return;
}
context.HttpContext.Session.Remove(UniqFormuId);
context.HttpContext.Session.SetString(UniqFormuId, currentFormId);
await context.HttpContext.Session.CommitAsync();
}
}
usage
[HttpPost]
[PreventDoublePost]
public async Task<IActionResult> Edit(EditViewModel model)
{
if (!ModelState.IsValid)
{
//PreventDoublePost Attribute makes ModelState invalid
}
throw new NotImplementedException();
}

Using ClaimsTransformer to set user permissions creates lengthy cookies and rejected by browsers

In our Aspnet core 1.1 app, we are using ClaimsTransformer to set permissions for the current user at the start of each request. It appears the cookie got bigger due to this and sometimes I see a warning in browser's console that the cookie was rejected as it was too long. Is it possible to add claims to identity without being passed down to the client through cookies? If not, is it possible to replace the cookie value with a Guid or some identifier and retain the lengthy cookie in the server itself or possibly DB?
This is how our code looks like
public class ClaimsTransformer : IClaimsTransformer
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var principal = context.Principal;
if (!principal.Identity.IsAuthenticated)
return principal;
var identity = (ClaimsIdentity)principal.Identity;
var db = context.Context.RequestServices.GetService<IDb>();
var entity = await db.Entity.FirstAsync(/*Add conditions here*/);
//This list is expected to grow with time.
identity.AddClaim(new Claim("CanManageAPIs", entity.CanManageAPIs.ToString()));
identity.AddClaim(new Claim("CanManageUsers", entity.CanManageUsers.ToString()));
identity.AddClaim(new Claim("CanManageSystems", entity.CanManageSystems.ToString()));
return principal;
}
}
//Configure method in Startup.cs
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new ClaimsTransformer()
});
public static class ClaimsPrincipalExtensions
{
public static bool CanManageSystems(this ClaimsPrincipal principal)
{
var value = principal.GetClaimValue("CanManageSystems");
bool.TryParse(value, out bool result);
return result;
}
}
And use the claims in our views.
#if (User.CanManageSystems())
{
<!-- Render some HTML content here. -->
}
Update Sep 03
I am not able to reproduce the issue anymore with the same source code without any local changes. The length of the cookie with name .AspNetCore.Cookies remains constant at 3331 irrespective of the number of claims being added to the identity and they are not persisting either. I did see that earlier when I was able to reproduce the issue, there two more cookies with names .AspNetCore.Cookies1 and .AspNetCore.Cookies2. The only change that happened between then and now is a claim was removed by one of my colleagues in Azure AD B2C and I can't say for sure if that is the reason. Since I don't have access to that part of Azure, I will need to wait for him to get back to work on Tuesday ET to do some experimenting.
Update Sep 11
I was able to get back to this issue today and figured out what caused the problem. As I keep browsing through the pages, when I cross half way mark of cookie expiration (approx. 3mins out of 5mins I configured to test), a new cookie is issued. And this cookie includes all the claims that were added by ClaimsTransformer code above and persists across page requests. The claims added during the subsequent requests seem to not overwrite but instead add additional claims to the identity. So I updated my code to remove existing claim if detected and add new one instead. This solves my problem.
However I would like to know if this is the expected behavior and I am using the claims incorrectly. Or is it a bug that I stumbled upon?

SQL write to ASP.NET user table doesn't save

My setup:
ASP.NET 4.5 web api (on Azure) saving data to SQL db (also on Azure)
AngularJS web front end (another Azure web site)
When a user first signs up, I show them a "getting started intro". The intro is only supposed to run once - I log the timestamp of the intro launch date as a custom field in the ASP.NET user table.
Imagine my surprise when I log in (as a user would) and see the intro TWICE.
The AngularJS front end is properly sending the "intro viewed" message to the ASP.NET api, and the api responds with a success message. However, when I look at the raw data in the db, the timestamp is most definitely NOT updated. Consequently, the user will see the intro a second time (at which point the timestamp gets recorded in the db properly).
I have a crappy workaround. After the client requests an OAuth Bearer token from my server, the client then requests user information (to decide whether or not to show the tour). Waiting 100ms and then sending the "tour viewed" message back to the server masks the issue.
I've not seen ANY other issues storing data at any point. Because our db is on Azure, I can't hook up Profiler and the built in auditing doesn't give me any clues.
Is there something about requesting the token that leaves ASP.NET identity in a funny state? And it takes a brief wait before you can write to the table? Are custom fields that extend the base Identity setup prone to problems like this? Is the UserManager possibly doing something weird in its black box?
Does anyone have suggestions for how to continue debugging this problem? Or ever hear of anything like it?
Here's the relevant code that should be updating the "tour viewed" timestamp in the db:
[HttpPost, Route("UserInfo")]
public async Task<IHttpActionResult> UpdateUserInfo(UpdateBindingModel model)
{
var currentUser = UserManager.FindById(User.Identity.GetUserId());
if (model.FirstName != null)
{
currentUser.FirstName = model.FirstName;
}
if (model.LastName != null)
{
currentUser.LastName = model.LastName;
}
if (model.SetIntroViewCompleteDate)
{
currentUser.IntroViewCompleteDate = DateTime.UtcNow;
}
if (model.SetIntroViewLaunchDate)
{
currentUser.IntroViewLaunchDate = DateTime.UtcNow;
}
if (model.SetTipTourCompleteDate)
{
currentUser.TipTourCompleteDate = DateTime.UtcNow;
}
if (model.SetTipTourLaunchDate)
{
currentUser.TipTourLaunchDate = DateTime.UtcNow;
}
IdentityResult result = await UserManager.UpdateAsync(currentUser);
if (result.Succeeded)
{
var data = new UserInfoViewModel
{
FirstName = currentUser.FirstName,
LastName = currentUser.LastName,
IntroViewLaunchDate = currentUser.IntroViewLaunchDate
};
return Ok(data);
}
return InternalServerError();
}
UPDATE ********* 4/18
I've also tried to move completely away from UserManager stuff. I've tried the following modifications (pulling the user data from a table like I would access any other data), but it still behaves the same. I'm starting to think that putting custom fields on the ApplicationUser object is a bad idea...
New db retrieve and save looks like this:
ApplicationDbContext newContext = new ApplicationDbContext();
var currentUser = await (from c in newContext.Users
where c.Email == User.Identity.Name
select c).SingleOrDefaultAsync();
//update some values
await newContext.SaveChangesAsync();
Basically the problem might be with initialization of the `UserManager' and the fact that this class works on the db context so you need to persist changes to that context. Here is an example:
var userStore = new UserStore<ApplicationUser>(new MyDbContext());
var userManager = new UserManager(userStore);
That way you remember both manager and context. Then in your method you would normally call:
IdentityResult result = await userManager.UpdateAsync(currentUser);
followed by persisting this change to db context:
var dbContext = userStore.context;
dbContext.saveChanges();
Based on your comment that waiting 100ms masks the issue, I think you may have a problem with the multiple async await calls. Try running the calls synchronously and see if you still have the same issue. My guess is that the problem might go away. My experience has been that using async await can be tricky when you have calls to asynchronous methods that call other asynchronous methods. You may have code that is executing without the proper results returned.
Well, here's what I did to solve the problem. I totally de-coupled my custom user data from the built in ASP.NET identity stuff. I've now got a separate object (and therefore separate SQL table) that stores things like FirstName, LastName, LastActiveDate, etc, etc.
This has solved my problem entirely, though it has introduced another call to the database in certain situations. I've deemed it to be not a big enough performance issue to worry about. I'm left thinking that this was some sort of weird race condition involving the generation of a token for an ASP.NET identity user then quickly writing to an Azure SQL database - lord knows what it was exactly in my code that caused the problem.
If you've got a problem that's hard to solve, often the best plan is to change the problem.
Now I need to find a meta thread discussing what to do with bounty points when you've blown up the problem...

Categories

Resources