Hello I Have created custom ApiUser class which inherits from IdentityUser.
public class ApiUser : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
The question it is set on Creating new user, but how do I change it/update it later?
UserManager<ApiUser> userManager
_userManager does not seem to have any methotds to do so.
[HttpPost]
[Route("UpdateUserDetails")]
public async Task<IActionResult> UpdateUser(ApiUserUpdate apiUserUpdate)
{
var user = await _userManager.FindByEmailAsync(apiUserUpdate.Email);
if(user == null)
{
return BadRequest("No user found with given email");
}
user.FirstName = apiUserUpdate.FirstName;
user.LastName = apiUserUpdate.LastName;
await _userManager.UpdateAsync(user);
return Ok("user updated");
}
Related
It's my first project to ASP.NET Core Authentication and Authorization and I get this error when I'm trying to pass Enum to [Authorize] attribute :
Error CS1503 Argument 1: cannot convert from
'BasicAuthAPI.Entities.Role' to
'string'
Here is my controller method which gives this error:
[Authorize(Role.Admin)]
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
Role enum:
public enum Role
{
Admin,
User
}
User Entity:
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public Role Role { get; set; }
[JsonIgnore]
public string PasswordHash { get; set; }
}
And the _userService which I have mentioned in controller:
public class UserService : IUserService
{
private DataContext _context;
private IJwtUtils _jwtUtils;
private readonly AppSettings _appSettings;
public UserService(
DataContext context,
IJwtUtils jwtUtils,
IOptions<AppSettings> appSettings)
{
_context = context;
_jwtUtils = jwtUtils;
_appSettings = appSettings.Value;
}
public AuthenticateResponse Authenticate(AuthenticateRequest model)
{
var user = _context.Users.SingleOrDefault(x => x.Username == model.Username);
// validate
if (user == null || !BCryptNet.Verify(model.Password, user.PasswordHash))
throw new AppException("Username or password is incorrect");
// authentication successful so generate jwt token
var jwtToken = _jwtUtils.GenerateJwtToken(user);
return new AuthenticateResponse(user, jwtToken);
}
public IEnumerable<User> GetAll()
{
return _context.Users;
}
public User GetById(int id)
{
var user = _context.Users.Find(id);
if (user == null) throw new KeyNotFoundException("User not found");
return user;
}
}
How can I pass the Admin Role to [Authorize] attribute?
Either use string constants
public static class Role
{
public static string Admin = "Admin";
public static string User = "User";
}
or you can use nameof
[Authorize(nameof(Role.Admin))]
You can just call .ToString()
[Authorize(Role.Admin.ToString())]
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
Looking at the answer from Alexander I have found the following SO post which highlights the difference between nameof and ToString: What is the difference between MyEnum.Item.ToString() and nameof(MyEnum.Item)?
I'm trying to add a user in my database but when I create it it is not added and it doesn't give me an error message. When I add a breakpoint to the CeateUser method the code is running but when I add a breakpoint on the result condition, the code is never reached.
Here's my PageModel
public class AddUserModel : PageModel
{
[BindProperty]
public NewAccountInput AccountInput { get; set; }
private UserManager<AdminUser> UserManager { get; set; }
public AddUserModel(UserManager<AdminUser> userManager)
{
UserManager = userManager;
}
public void OnPost()
{
if(AccountInput.ComfirmationPassword != AccountInput.Password)
{
return;
}
_ = CreateUser();
}
public async Task<bool> CreateUser()
{
var result = await UserManager.CreateAsync(new AdminUser()
{
Email = AccountInput.Email,
FirstName = AccountInput.UserName,
LastName = AccountInput.UserName,
UserName = AccountInput.Email
}, AccountInput.Password);
if (result.Succeeded)
{
return true;
}
else
{
return false;
}
}
}
The solution is because the method OnPost must be of type Task.
public async Task OnPost()
{
if(AccountInput.ComfirmationPassword != AccountInput.Password)
{
return;
}
await CreateUser();
}
I'm trying to get information from a demo database using a RESTful api built in ASP.NET. In order to get information from the database, I need to pass in an object trough the header. However, my API is not receiving those values. My API function is using the [FromHeader] attribute but nothing comes out. I get a 400 status with the following error message: {"AccountEmail":["The AccountEmail field is required."]}
So here is the following code:
I have a model that looks like this:
{
[Serializable]
public class Account
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Required]
public long AccountId { get; protected set; }
[Required]
[EmailAddress]
public string AccountEmail { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Required]
public Guid AccountAccessKey { get; set; }
}
}
The API call looks like
[HttpGet]
[AccountFilter]
public async Task<ActionResult<Account>> GetAccount([FromHeader] Account account) {
try
{
return await _Context.GetAccount(account.AccountEmail);
}
catch { }
return BadRequest(new Error()
{
ErrorTitle = "Unable to get Account",
ErrorMessage = "ValidationError"
});
}
Where GetAccount looks like
public Task<Account> GetAccount(string email)
{
return _context.Accounts.FirstOrDefaultAsync(x => x.AccountEmail == email);
}
The API call looks like this
let account = new Account({
AccountEmail: 'gazefekini#eaglemail.top',
AccountAccessKey:'00000000-0000-0000-0000-000000000000',
AccountId:0
})
fetch(process.env.API + '/api/Account/GetAccount', {
method:'GET',
headers: {
"Accept":"application/json",
"account": "" + account
}
})
I could use FromQuery and get the account trough the AccountId, but I have a Filter that needs the AccountAccessKey and the AccountEmail to do some AWS verification in the Filter, hence why I decided to pass an account header to the api call.
I tried removing the [Required] attribute to the AccountEmail in the model, and then the API call works, but I need to have a [Required] attribute.
Am I missing something? I understand that the Error is coming from that [Required] attribute but I'm not sure why
For your requirement, you need to implement your own model binder.
Custom HeaderComplexModelBinder
public class HeaderComplexModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var headerModel = bindingContext.HttpContext.Request.Headers[bindingContext.OriginalModelName].FirstOrDefault();
var modelType = bindingContext.ModelMetadata.ModelType;
bindingContext.Model = JsonConvert.DeserializeObject(headerModel, modelType);
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
}
Custom IModelBinderProvider
public class HeaderComplexModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType)
{
var x = context.Metadata as Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata;
var headerAttribute = x.Attributes.Attributes.Where(a => a.GetType() == typeof(FromHeaderAttribute)).FirstOrDefault();
if (headerAttribute != null)
{
return new BinderTypeModelBinder(typeof(HeaderComplexModelBinder));
}
else
{
return null;
}
}
else
{
return null;
}
}
}
Configure in Startup.cs
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new HeaderComplexModelBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
I've added a few specific Properties to the ApplicationUser of the standart project.
some of the added Properties are of custom classes.
Since i use EntityFramework, it creates a dbtable for users and one for each custom class.
i added the Properties to my ManageController and Views and adding these Properties to the specific dbtable works, but i cant access them. in the dbo.AspNetUsers there is a column added, that is called after the attribute + ID (In my example "NameID").
Now if i am loading the user in my ManageController, every normal Attribute is loaded, but the custom ones are null.
My Question is, how can i load the custom objects (that are really stored in the other table).
ApplicationUser.cs:
namespace refProject.Models
{
public class ApplicationUser : IdentityUser
{
public Name Name { get; set; }
}
}
ManageController.cs
//other usings
using refProject.Models;
using refProject.Models.ManageViewModels;
namespace refProject.Controllers
{
[Authorize]
public class ManageController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
//other managers
public ManageController(
UserManager<ApplicationUser> userManager,
//other managers
)
{
_userManager = userManager;
//other managers
}
//
// GET: /Manage/Index
[HttpGet]
public async Task<IActionResult> Index(ManageMessageId? message = null)
{
ViewData["StatusMessage"] =
message == ManageMessageId.ChangeNameSuccess ? "Your name has been changed."
: message == ManageMessageId.SetNameSuccess ? "Your name has been set."
: "";
var user = await GetCurrentUserAsync();
if (user == null)
{
return View("Error");
}
var model = new IndexViewModel
{
//other Properties
//
//
// THIS ONE IS NULL
//
//
Name = user.Name
//other Properties
};
return View(model);
}
// GET: /Manage/ChangeName
[HttpGet]
public IActionResult ChangeName()
{
return View();
}
//
// POST: /Manage/ChangeName
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangeName(ChangeNameViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await GetCurrentUserAsync();
if(user != null)
{
Name NewName = new Name();
NewName.FirstName = model.NewFirstName;
NewName.LastName = model.NewLastName;
user.Name = NewName;
IdentityResult result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangeNameSuccess });
}
AddErrors(result);
return View(model);
}
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error });
}
//
// GET: /Manage/SetName
[HttpGet]
public IActionResult SetName()
{
return View();
}
//
// POST: /Manage/SetName
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetName(SetNameViewModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
var user = await GetCurrentUserAsync();
if(user != null)
{
Name NewName = new Name();
NewName.FirstName = model.NewFirstName;
NewName.LastName = model.NewLastName;
user.Name = NewName;
IdentityResult result = await _userManager.UpdateAsync(user);
if(result.Succeeded)
{
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetNameSuccess });
}
AddErrors(result);
return View(model);
}
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error });
}
#region Helpers
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
public enum ManageMessageId
{
Error,
ChangeNameSuccess,
SetNameSuccess,
}
private Task<ApplicationUser> GetCurrentUserAsync()
{
return _userManager.GetUserAsync(HttpContext.User);
}
#endregion
}
}
Name.cs
namespace refProject.Models
{
public class Name
{
public int ID { get; set; }
public string fTitle { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string lTitle { get; set; }
public override string ToString()
{
return fTitle + " " + FirstName + " " + LastName + " " + lTitle;
}
}
}
That is a known issue. It is not considered a bug, but rather a design decision.
The recommended way is to access the user through DbContext rather than from the UserManager implementation.
"Just to add a bit more detail: as a performance optimization ASP.NET Core Identity currently only loads the entities related to a user or a role as needed to satisfy API calls. I.e. it won't load related entities (not even the built-in ones) eagerly on a method call like such as FindByName() because the find methods are only required to return the root.
At this point issuing queries against the DbContext is the recommended
way to load related data. If you want to abstract this from the
application code you can extend both the Identity store and manager
classes to add methods to retrieve and return your custom related
data."
Comment link
You could change your GetCurrentUserAsync method as follows:
private ApplicationUser GetCurrentUserAsync()
{
return _userManager.Users.Include(x => x.Name).FirstOrDefault(x => x.Id == _userManager.GetUserId(User));
}
I've made custom User and UserStore classes.
Now I'm trying to register or login with a user, but I get the error
'Incorrect number of arguments supplied for call to method Boolean
Equals(System.String, System.String, System.StringComparison)'
The error is on line 411:
Line 409: }
Line 410: var user = new User() { UserName = model.Email, Email = model.Email };
--> Line 411: IdentityResult result = await UserManager.CreateAsync(user);
Line 412: if (result.Succeeded)
Line 413: {
I get this same error on
UserManager.FindAsync(user), UserManager.CreateAsync(user,password)
Now the error doesn't occur when I log in with an External Login, like Google, which also uses methods from UserManager. Entering the email works as well, but when the user has to be created from an External Login with the inserted email, it gives the CreateAsync error too.
EDIT The error also occurs on UserManager.Create(User user)
This is probably because the method in UserManager does an Equal on the user object's id, while it is expecting a string and mine is an int. But because I can't get a stacktrace inside the UserManager, and I have no idea how to override this method in the UserManager, I do not know how to solve this?
How can I fix this? Do I need to create my own UserManager? Or do I need another solution entirely?
My userstore code:
public class UserStore :
IUserStore<User>,
IUserPasswordStore<User>,
IUserSecurityStampStore<User>,
IUserEmailStore<User>,
IUserLoginStore<User>
{
private readonly NFCMSDbContext _db;
public UserStore(NFCMSDbContext db)
{
_db = db;
}
public UserStore()
{
_db = new NFCMSDbContext();
}
#region IUserStore
public Task CreateAsync(User user)
{
if (user == null)
throw new ArgumentNullException("user");
_db.Users.Add(user);
_db.Configuration.ValidateOnSaveEnabled = false;
return _db.SaveChangesAsync();
}
public Task DeleteAsync(User user)
{
if (user == null)
throw new ArgumentNullException("user");
_db.Users.Remove(user);
_db.Configuration.ValidateOnSaveEnabled = false;
return _db.SaveChangesAsync();
}
public Task<User> FindByIdAsync(string userId)
{
int userid;
if(int.TryParse(userId,out userid))
throw new ArgumentNullException("userId");
return _db.Users.Where(u => u.UserId == userid).FirstOrDefaultAsync();
}
(...)
User.cs:
public sealed class User : IUser<int>
{
public User()
{
UserLogins = new List<UserLogin>();
}
public int UserId { get; set; }
public string UserName { get; set; }
public string PasswordHash { get; set; }
public string SecurityStamp { get; set; }
public string Email {get; set; }
public bool IsEmailConfirmed { get; set; }
int IUser<int>.Id
{
get { return UserId; }
}
public ICollection<UserLogin> UserLogins { get; private set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
Did you change your UserManager type to UserManager to tell it that your user key is now int instead of string?