Asked by:
Identity Confirm Email issue

Question
-
User986042657 posted
Looking for some input into an issue I have come across and being new to development I'm having an issue finding a solution.
I'm using Microsoft Identity to manage user registrations and sign ins. When trying to build the confirm email section of this, I am running across an error stating:
Argument 1: cannot convert from 'string' to 'Project.API.Models.User'
This is being thrown in the controller. Here are the relevant files:
IAuthRepository:
using System.Threading.Tasks; using Outmatch.API.Models; namespace Outmatch.API.Data { public interface IAuthRepository { // Register the user // Return a task of User. We will call this method Register. The method is passed a User object and string of Password Task<User> Register(User user, string password); // Login the user to the API // Return a User. We will call this method Login, pass a string of username, and a string of Password Task<User> Login(string username, string password); // Check if the user already exists // Return a boolean task, call the method UserExists. Check against a string of username to see if that username already exists. Task<bool> UserExists(string username); // Check Task<User> ConfirmEmailAsync(string userId, string token); } }
AuthRepository:
using System; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Outmatch.API.Models; namespace Outmatch.API.Data { public class AuthRepository : IAuthRepository { // Inject the DataContext so the AuthRepository can query the database private readonly DataContext _context; private readonly UserManager<User> _userManager; private readonly IConfiguration _configuration; private readonly IMailRepository _mailRepository; public AuthRepository(DataContext context, UserManager<User> userManager, IConfiguration configuration, IMailRepository mailRepository) { _mailRepository = mailRepository; _configuration = configuration; _userManager = userManager; _context = context; } // Login the user. Return a task of type User. Takes a string of username and a string of password // Use the username to identify the user in the db. Take the password and compare it with the hashed password of the user to autenticate public async Task<User> Login(string username, string password) { // Created a variable to store the user in. _context.Users signifies the Users table in the db // This will look in the DB for a user that matches the entered username and return it, or return null if it doesnt exist var user = await _context.Users.FirstOrDefaultAsync(x => x.UserName == username); // If the user is returned null from the DB (IE, username does not exist), then return null. This would return a 401 not authorized in the browser if (user == null) return null; // Return true or false depending on weather the password matches or doesnt match what the user supplied when loggin in (in a hashed format) // If the password returns null, the we will return null and a 401 not authorized in the browser // Added Microsoft Identity, Signin manager will now take care of the below // if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) // return null; // If the comparison of the verifyPasswordHash method returns true, then return the user. return user; } private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt) { // Use the password salt as a key to hash and salt the password of the user logging in, so it can be compared with the password in the DB. using (var hmac = new System.Security.Cryptography.HMACSHA512(passwordSalt)) { // Compute the hash from the password, using the key being passed. Comupted has will be the same as the Register method hash var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); // Loop through the hashed password and compare it to the eash element in the array to ensure the has matches what is stores in the DB for (int i = 0; i < computedHash.Length; i++) { if (computedHash[i] != passwordHash[i]) return false; } } // If each element of the password hash array matches, return true return true; } // Takes the user model (entity) and their chosen password. public async Task<User> Register(User user, string password) { //Turn the password which is in plain text and store is as a salted hash byte[] passwordHash, passwordSalt; // We want to pass the passwordHash and passwordSalt as a referece, and not as a value. So this will be done using thr out keyword CreatePasswordHash(password, out passwordHash, out passwordSalt); // Forward the new user to the database to be stored. await _context.Users.AddAsync(user); await _context.SaveChangesAsync(); // Generate a token to be used for the user to confirm their email. var confirmEmailToken = await _userManager.GenerateEmailConfirmationTokenAsync(user); var encodedEmailToken = Encoding.UTF8.GetBytes(confirmEmailToken); var validEmailToken = WebEncoders.Base64UrlEncode(encodedEmailToken); string url = $"{_configuration["AppUrl"]}/api/auth/confirmemail?userId={user.Id}&token={validEmailToken}"; await _mailRepository.SendEmailAsync(user.Email, "Confirm Your Email", "<h1><Welcome to Outmatched.</h1>" + $"<p>Please confirm your email by <a herf='{url}'> clicking here</a></p>"); return user; } private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) { // Hash the password using SHA512 using (var hmac = new System.Security.Cryptography.HMACSHA512()) { // Set the password salt to a randomly generated key passwordSalt = hmac.Key; // Compute the hash passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); // Once complete, the values are stores in the byte[] array variabled just a few lines up } } // Check to see if the username exists in the databse. public async Task<bool> UserExists(string username) { if (await _context.Users.AnyAsync(x => x.UserName == username)) return true; return false; } // Confirm a users email address after it is registered. public async Task<User> ConfirmEmailAsync(string userId, string token) { throw new NotImplementedException(); } } }
AuthController:
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using Outmatch.API.Data; using Outmatch.API.Dtos; using Outmatch.API.Models; namespace Outmatch.API.Controllers { // Route will be api/auth (http://localhost:5000/api/auth) [Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { // Inject the auth repository and the programs configuration into the controller. private readonly IConfiguration _config; private readonly IMapper _mapper; private readonly SignInManager<User> _signInManager; private readonly UserManager<User> _userManager; private readonly IClientRepository _repo; private readonly IMailRepository _MailRepository; public AuthController(IConfiguration config, IMapper mapper, UserManager<User> userManager, SignInManager<User> signInManager, IClientRepository repo, IMailRepository MailRepository) { _MailRepository = MailRepository; _repo = repo; _userManager = userManager; _signInManager = signInManager; _mapper = mapper; _config = config; } // Create a new HTTP Post method (http://localhost:5000/api/auth/register) to login the user. JSON Serialized Object will be passed // from the user when they enter it to sign in. Call the Username from the UserForRegisterDto class [Authorize(Policy = "RequireGlobalAdminRole")] [HttpPost("register")] public async Task<IActionResult> Register(UserForRegisterDto userForRegisterDto) { // Check if user already exists var newUser = await _userManager.FindByNameAsync(userForRegisterDto.username); if (newUser != null) return BadRequest("Username already exists"); // If the user does not already exist, create the user and use AutoMapper to map the details to the database var userToCreate = _mapper.Map<User>(userForRegisterDto); var result = await _userManager.CreateAsync(userToCreate, userForRegisterDto.Password); var userToReturn = _mapper.Map<UserForRegisterDto>(userToCreate); if (result.Succeeded) { // Add client to organization await addClientToOrg(userToCreate.Id, userForRegisterDto.OrgId); // Send back a location header with the request, and the ID of the user. return CreatedAtRoute("GetUser", new { controller = "Users", Id = userToCreate.Id }, userToReturn); } return BadRequest(result.Errors); } // Assign a client to an organization [HttpPost("{userId}/clienttoorg/{organizationId}")] public async Task<IActionResult> addClientToOrg(int userId, int organizationId) { // Check if the user assigning the client to the org has the authoirization to do so var userRole = User.FindFirst(ClaimTypes.Role).ToString(); if (userRole != "http://schemas.microsoft.com/ws/2008/06/identity/claims/role: GlobalAdmin") return Unauthorized(); var association = await _repo.GetUserOrg(userId, organizationId); // Check if the user is already assigned to the organization in the database if (association != null) return BadRequest("This client is already associated with this organization"); // checked if the organization exists or not if (await _repo.GetUser(organizationId) == null) return BadRequest("The selected organization the user is being assigned to does not exist"); // Assign the assiciiation to an OrgToClient object association = new OrgToClients { UserId = userId, OrganizationId = organizationId }; // Pass the client to the organization _repo.Add<OrgToClients>(association); // Save the information to the OrgToClient tabe if (await _repo.SaveAll()) return Ok(); // If unable to save to the table, pass an error to the user return BadRequest("Failed to add user to an organization"); } // Create a method to allow users to login to the webAPI by returning a token to the users once logged in. // Route will be http://localhost:5000/api/auth/login [AllowAnonymous] [HttpPost("login")] public async Task<IActionResult> Login(UserForLoginDto userForLoginDto) { // Check that the user trying to login exists var user = await _userManager.FindByNameAsync(userForLoginDto.Username); if (user == null) return Unauthorized(); var result = await _signInManager.CheckPasswordSignInAsync(user, userForLoginDto.Password, false); // Check to see if there is anything inside the user from Repo. If there is, a user object is present. If not, the username doesnt exist. if (result.Succeeded) { var appUser = _mapper.Map<ClientForListDto>(user); var currentDate = DateTime.UtcNow; if (currentDate <= user.EndDate) { // await _MailRepository.SendEmailAsync(user.UserName, "New Login", "<h1>You have successfully logged in to you Instacom account</h1> <p>New login to your account at " + DateTime.Now + "</p>"); // Return the token to the client as an object return Ok(new { token = GenerateJwtToken(user).Result, user = appUser }); } } return Unauthorized(); } private async Task<string> GenerateJwtToken(User user) { // Build a token that will be returned to the user when they login. Contains users ID and their username. var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName), // new Claim(ClaimTypes.Role, userFromRepo.AccessLevel) - THIS IS A ROLE BASED KEY FOR USER ACCESS. NOT USED AND LOOKING FOR AN ALTERNATIVE }; // Get a list of roles the user is in var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } // Create a secret key to sign the token. This key is hashed and not readable in the token. var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.GetSection("AppSettings:Token").Value)); // Generate signing credentials var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); // Create a security token descriptor to contain the claims, exp date of the token, and signing credentials for the JWT token var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.Now.AddDays(1), SigningCredentials = creds }; // Generate Token Handler var tokenHandler = new JwtSecurityTokenHandler(); // Create a token and pass in the token descriptor var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } [HttpGet("confirmemail")] public async Task<IActionResult> ConfirmEmail(string userId, string token) { if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(token)) return NotFound(); var result = await _userManager.ConfirmEmailAsync(userId, token); } } }
User Model:
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Identity; namespace Outmatch.API.Models { // List of properties for the User (Client) table in the db public class User : IdentityUser<int> { public string FirstName { get; set; } public string LastName { get; set; } public DateTime ActiveDate { get; set; } public DateTime EndDate { get; set; } // User Roles Management public virtual ICollection<UserRole> UserRoles { get; set; } // Organization to Client table ties public ICollection<OrgToClients> OrganizationId { get; set; } } }
The error in appearing in the AuthController on the 4th last line under userId:
var result = await _userManager.ConfirmEmailAsync(userId, token);
Any ideas on how I may be able to solve this? Any input is appreciated!
Thursday, April 30, 2020 3:47 AM
All replies
-
User475983607 posted
The error is very clear. You're code tries to assign a string to a Project.API.Models.User type. I'm a bit confused why you did not share the exact line of code that cause this issue. You expect the community to go through all your code?
Thursday, April 30, 2020 11:37 AM -
User986042657 posted
Hey Mgebhard.
I thought I was extremely clear on where the error was. The end of my post not only says where the error is, but also contains a screenshot of it. Perhaps you missed that
Thursday, April 30, 2020 4:50 PM -
User475983607 posted
The ConfirmEmailAsync() expects a User type not a string.
Get the user first and pass the user to the method.
Thursday, April 30, 2020 4:57 PM