locked
Identity Confirm Email issue RRS feed

  • 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!

    Snapshot of the error

    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.

    https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.usermanager-1.confirmemailasync?view=aspnetcore-3.1

    Get the user first and pass the user to the method.

    Thursday, April 30, 2020 4:57 PM