Asp.net Core Role Expiration - c#

Is there a way to remove a user from a role after a given timespan? When I try something like the below code, I get a null exception once the Delay continues in sessionExpired()...
public async Task<IActionResult> PurchaseSession(PurchaseSessionViewModel model)
{
var user = await _userManager.GetUserAsync(User);
await _userManager.AddToRoleAsync(user, "Active");
await _signInManager.RefreshSignInAsync(user);
// no await
sessionExpired(user);
return RedirectToAction(nameof(Index));
}
private async void sessionExpired(ApplicationUser user)
{
await Task.Delay(10000);
await _userManager.RemoveFromRoleAsync(user, "Active");
}
Note, I understand why the exception occurs but I'd like to retain this type of role-based authorization since [Authorize(Roles = "Active")] provides the functionality I'm after. Is there another way to do this?

Your problem is that your user variable is local and thus is deleted after your first function ends.
You may use a closure (as a lambda function). It is a block of code which maintains the environment in which it was created, so you can execute it later even if some variables were garbage collected.
EDIT: If you want to know why my previous solution didn't work, it is probably because user was being disposed by Identity at a time or another, so here is another try:
public async Task<IActionResult> PurchaseSession(PurchaseSessionViewModel model)
{
var user = await _userManager.GetUserAsync(User);
await _userManager.AddToRoleAsync(user, "Active");
await _signInManager.RefreshSignInAsync(user);
// We need to store an ID because 'user' may be disposed
var userId = user.Id;
// This create an environment where your local 'userId' variable still
// exists even after your 'PurchaseSession' method ends
Action sessionExpired = async () => {
await Task.Delay(10000);
var activeUser = _userManager.FindById(userId);
await _userManager.RemoveFromRoleAsync(activeUser, "Active");
};
Task.Run(sessionExpired);
return RedirectToAction(nameof(Index));
}

Related

Iterating through async task to update user role

I am writing an Edit method for my C.R.U.D application but I am stuck with updating user role. I am trying to iterate through user roles and assign user role is not already assigned role. I get an error saying,foreach cannot operate on variables of type 'Task<IList<string>>' because 'Task<IList<string>>' does not contain a public instance definition for 'GetEnumerator' How can I fix this ? OR What is a better way to approach this?
[HttpPost]
public async Task<IActionResult> Edit(EditUserViewModel editUserViewModel)
{
var user = _userManager.Users.Where(e => e.Id == editUserViewModel.id).SingleOrDefault();
user.FirstName = editUserViewModel.FirstName;
user.LastName = editUserViewModel.LastName;
user.MiddleName = editUserViewModel.MiddleName;
user.Dob = editUserViewModel.Dob;
var AllUserRoles = _userManager.GetRolesAsync(user);
foreach(var userRole in AllUserRoles)
{
if (userRole != editUserViewModel.RoleToBeAssigned)
{
await _userManager.AddToRoleAsync(user, editUserViewModel.RoleToBeAssigned);
}
}
await _userManager.UpdateAsync(user);
return View(user);
}
The GetRolesAsync returns a Task that represents the asynchronous operation, containing a list of role names. So you can't directly iterate on that using a foreach. You should use the await keyword to wait the result and than iterate. Example: var AllUserRoles = await _userManager.GetRolesAsync(user);
You can see here the GetRolesAsync method details.

ASP MVC ConfirmEmailAsync not working

I register a user, receive a token via email which looks like this:
Please confirm your account by clicking here
I click the link and I can see that the ConfirmEmail method in AccountController fires:
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
And that result.Succeeded is true.
Everything appears fine, but when trying to log in after completing this process I get taken to the page telling me my account is locked
Locked out.
This account has been locked out, please try again later.
What couldI be doing wrong? Do I need to manually change the lockout flag in the db? If so, what is the point of the ConfirmEmailAsync method?
ConfirmEmailAsync just sets the EmailConfirmed on the user account record to true. From UserManager (edited for brevity):
public virtual async Task<IdentityResult> ConfirmEmailAsync(TUser user, string token)
{
...
var store = GetEmailStore();
...
await store.SetEmailConfirmedAsync(user, true, CancellationToken);
return await UpdateUserAsync(user);
}
Where GetEmailStore returns the IUserEmailStore (which is implemented by UserStore by default), which sets the flag:
public virtual Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
...
user.EmailConfirmed = confirmed;
return Task.CompletedTask;
}
The error you're getting indicated that the LockoutEnabled flag on the user account is true. You can set this to false by calling the SetLockoutEnabledAsync method on the UserManager.
There is also a SupportsUserLockout flag on the UserManager which unlocks accounts by default on creation. In order to set this you will need to create your own UserManager and override this flag to false.
At first, I had challenges getting these to work and after a series of research no success. Finally, I got the root of the problem(s) and fixed them thus sharing my experience. Follow the following process and I am sure it will help.
Step 1
Goto Startup.cs and remove the code below if you have it initialised;
services.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
//options.LowercaseQueryStrings = true; //(comment or remove this line)
});
Step 2 For GenerateEmailConfirmationTokenAsync() / ConfirmEmailAsync()
2a. On registering new user for token generation go as thus;
var originalCode = await userManager.GenerateEmailConfirmationTokenAsync(user);
var code = HttpUtility.UrlEncode(originalCode);
var confirmationLink = Url.Action("ConfirmEmail", "Account",
new { userId = user.Id, token = code }, Request.Scheme);
2b. On receiving confrimationLink for email confirmation, go as thus
var originalCode = HttpUtility.UrlDecode(token);
var result = await userManager.ConfirmEmailAsync(user, originalCode);
if (result.Succeeded)
{
return View(); //this returns login page if successful
}
For GeneratePasswordResetTokenAsync() and ResetPasswordAsync()
a.
var originalCode = await userManager.GeneratePasswordResetTokenAsync(user);
var code = HttpUtility.UrlEncode(originalCode);
var passwordResetLink = Url.Action("ResetPassword", "Account",
new { email = model.Email, token = code }, Request.Scheme);
b.
var orginalCode = HttpUtility.UrlDecode(model.Token);
var result = await userManager.ResetPasswordAsync(user, orginalCode, model.Password);
if (result.Succeeded)
{
return View("ResetPasswordConfirmation");
}

BotFramework: PrivateConversationData or UserData becomes empty

In MessagesController.cs, following code is executed in Post method,
if (activity.Text.ToLowerInvariant().StartsWith("code:"))
{
var stateClient = activity.GetStateClient();
var botData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
var token = botData.GetProperty<string>("AccessToken");
BotUserModel botUser = CreateNewUser(token);
var privateData = await stateClient.BotState.GetPrivateConversationDataAsync(activity.ChannelId, activity.Conversation.Id, activity.From.Id);
privateData.SetProperty<BotUserModel>("botUser", botUser);
}
else
{
await Conversation.SendAsync(activity, () => new LuisDialog());
}
This is saving botUser into PrivateConversationData dictionary
Inside the LUIS Dialog,
[LuisIntent("DoSomething")]
public async Task DoSomething(IDialogContext context, LuisResult result)
{
BotUserModel botUser;
context.PrivateConversationData.TryGetValue<BotUserModel>("botUser", out botUser);
// Just to test
context.PrivateConversationData.SetValue<BotUserModel>("TestValue", new BotUserModel());
}
Here, I'm getting an exception KeyNotFoundException:botUser
BotUserModel is marked [Serializable] and has few public properties - all with get/set. I checked the IBotBag (i.e. PrivateConversationData) and its empty
[LuisIntent("DoSomethingNew")]
public async Task DoSomethingNew(IDialogContext context, LuisResult result)
{
// Assuming DoSomething intent is invoked first
BotUserModel botUser;
context.PrivateConversationData.TryGetValue<BotUserModel>("TestValue", out botUser);
// Here, no exception!
}
Now, here I get the value of TestValue set in LUIS Dialog in DoSomething method.
So essentially, any data set to PrivateConversationData or UserData inside LUIS Intent is accessible by other LUIS intents; whereas, data set in MessageController.cs (before LUIS is called) is not accessible within LUIS.
Tried with UserData as well.
Am I missing anything?
You are forgetting to set the private data store back into the state client. This should make it work.
var privateData = await stateClient.BotState.GetPrivateConversationDataAsync(activity.ChannelId, activity.Conversation.Id, activity.From.Id);
privateData.SetProperty<BotUserModel>("botUser", botUser);
await stateClient.BotState.SetPrivateConversationDataAsync(activity.ChannelId, activity.Conversation.Id, activity.From.Id, privateData);
Check out the documentation on the state client.

Refreshing claimsPrincipal after changing roles

I'm having some issues with changing role in dotnetcore identity.
I have the following code.
private async Task SetRoleToX(ClaimsPrincipal claimsPrincipal, string X)
{
var currentUser = await UserManager.GetUserAsync(claimsPrincipal);
var roles = await UserManager.GetRolesAsync(currentUser);
await UserManager.RemoveFromRolesAsync(currentUser, roles);
await UserManager.AddToRoleAsync(currentUser, X);
await SignInManager.RefreshSignInAsync(currentUser);
}
I cannot get the ClaimsPrincipal to update.
I have tried using sign in and sign out.
The role switch works fine if I manually sign in and out.
I have been searching the web and alot of people say this should work :(
Rather annoyingly all I had to do was send the token back with the request.
I cant believe i didn't think of it hope this helps someone.
Update with some code as requested
// In controller
public async Task SwapRole([FromBody]RoleSwapRequestDto dto)
{
await _service.SwapRole(
User,
dto.RoleName
);
return await AddCookieToResponse();
}
private async Task AddCookieToResponse()
{
// Make your token however your app does this (generic dotnet core stuff.)
var response = await _tokenService.RegenToken(User);
if (response.Data != null && response.Data.Authenticated && response.Data.TokenExpires.HasValue)
{
Response.Cookies.Append(AuthToken, response.Data.Token, new CookieOptions
{
HttpOnly = false,
Expires = response.Data.TokenExpires.Value
});
}
return response;
}
/// inside _service
public async Task SwapRole(ClaimsPrincipal claimsPrincipal, string X)
{
var currentUser = await UserManager.GetUserAsync(claimsPrincipal);
var roles = await UserManager.GetRolesAsync(currentUser);
await UserManager.RemoveFromRolesAsync(currentUser, roles);
await UserManager.AddToRoleAsync(currentUser, X);
await SignInManager.RefreshSignInAsync(currentUser);
}

Waiting for async function to finish

I have a problem, I want to make the asynchrnous function to finish first before going to the next one. Why? Because the value of the function is needed to the next line of code.
I tried, await, wait(), RunSynchronous but it is not working.
It still proceeds to execute other lines of code.
This is my code: It is the default Login for ASP.NET MVC5 but what I want to do is change the redirect depending on the user.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
SignInAsync(user, model.RememberMe);
RedirectToLocal(LogInRedirect());
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
I have a custom parameter called UserDepartment and I can only retrieve it after SignInAsync(user, model.RememberMe); finish executing.
This is the code for SignInAsync
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
What should I do to achieve my goal that SignInAsync should finish first before proceeding to other line of code.
EDIT:
So await works, but there is another problem.
SignInAsync is not yet finish but it already return that it is completed. By finish I mean the execution of code, I trace the execution but there are times that AuthenticationManager.SignIn is not yet finish but still return it is
You need to await the result of the SignInAsync call
await SignInAsync(user, model.RememberMe);
...
You internally await the result of the UserManager.CreateIdentityAsync method so that will block until that result returns inside the SignInAsync method. However, without awaiting the call to SignInAsync itself, the code won't block which is why the next line is called immediately.

Categories

Resources