Search code examples
c#encryptionasp.net-core-mvcasp.net-core-2.0asp.net-core-mvc-2.0

ASP.NET Core MVC Encrypt/Decrypt QueryString Values


I'm using ASP.NET Core 2.0 MVC, C#, Entity Framework Core Code First and SQL Server 2016.

I created a web form and all of my CRUD operations are working fine. However, I need some help on Encrypting / Decrypting the Querystring values that get passed to the Confirmation, Edit and Delete Views.

On my Index page I would also like to Encrypt the EmployeeID when a user hovers over the Edit and Delete links to those Action Methods. I don't want them to be visible on the Index Page.

Please see my code that I have below.

Models

public class Employee
{
    public int EmployeeID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public int DepartmentID { get; set; }
    public Department Department { get; set; }

    public int AppointmentID { get; set; }
    public Appointment Appointment { get; set; }
}

public class Department
{
    public int DepartmentID { get; set; }
    public string Name { get; set; }

    public ICollection<Employee> Employees { get; set; }
}

public class Appointment
{
    public int AppointmentID { get; set; }
    public string TimeSlot { get; set; }

    public ICollection<Employee> Employees { get; set; }
}

DbContext

public class WinTenDbContext : DbContext
{
    public WinTenDbContext(DbContextOptions<WinTenDbContext> options) : base(options)
    { }

    public DbSet<Employee> Employees { get; set; }
    public DbSet<Department> Departments { get; set; }
    public DbSet<Appointment> Appointments { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Employee>()
            .HasKey(e => e.EmployeeID);

        modelBuilder.Entity<Employee>()
            .Property(e => e.FirstName)
            .HasColumnType("varchar(50)")
            .HasMaxLength(50)
            .IsRequired();

        modelBuilder.Entity<Employee>()
            .Property(e => e.LastName)
            .HasColumnType("varchar(50)")
            .HasMaxLength(50)
            .IsRequired();

        modelBuilder.Entity<Department>()
            .HasKey(d => d.DepartmentID);

        modelBuilder.Entity<Department>()
            .Property(d => d.Name)
            .HasColumnType("varchar(50)")
            .HasMaxLength(50);

        modelBuilder.Entity<Appointment>()
            .HasKey(a => a.AppointmentID);

        modelBuilder.Entity<Appointment>()
            .Property(a => a.TimeSlot)
            .HasColumnType("varchar(50)")
            .HasMaxLength(50);
    }
}

ViewModels

public class EmployeeFormVM
{
    public int EmployeeID { get; set; }

    [Required(ErrorMessage = "Please enter your First Name")]
    [Display(Name = "First Name")]
    [StringLength(50)]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "Please enter your Last Name")]
    [Display(Name = "Last Name")]
    [StringLength(50)]
    public string LastName { get; set; }

    [Required(ErrorMessage = "Please select your Department")]
    [Display(Name = "Department")]
    public int DepartmentID { get; set; }

    public IEnumerable<Department> Departments { get; set; }

    [Required(ErrorMessage = "Please select your Appointment")]
    [Display(Name = "Appointment")]
    public int AppointmentID { get; set; }

    public IEnumerable<Appointment> Appointments { get; set; }
}

EmployeesController

public class EmployeesController : Controller
{
    private readonly WinTenDbContext _context;

    public EmployeesController(WinTenDbContext context)
    {
        _context = context;
    }

    //// GET: Employees
    //public async Task<IActionResult> Index()
    //{
    //    var winTenDbContext = _context.Employees.Include(e => e.Appointment).Include(e => e.Department);
    //    return View(await winTenDbContext.ToListAsync());
    //}

    public async Task<IActionResult> Index(string sortOrder, string currentFilter, string searchString, int? page)
    {
        ViewData["CurrentSort"] = sortOrder;
        ViewData["FirstNameSortParm"] = sortOrder == "fname" ? "fname_desc" : "fname";
        ViewData["LastNameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "lname_desc" : "";
        ViewData["DeptNameSortParm"] = sortOrder == "deptname" ? "deptname_desc" : "deptname";
        ViewData["DateSortParm"] = sortOrder == "time_slot" ? "time_slot_desc" : "time_slot";

        if (searchString != null)
        {
            page = 1;
        }
        else
        {
            searchString = currentFilter;
        }

        ViewData["CurrentFilter"] = searchString;

        var employees = from s in _context.Employees.Include(e => e.Appointment).Include(e => e.Department)
                        select s;

        if (!String.IsNullOrEmpty(searchString))
        {
            employees = employees.Where(s => s.LastName.Contains(searchString)
                                   || s.FirstName.Contains(searchString));
        }

        switch (sortOrder)
        {
            case "fname":
                employees = employees.OrderBy(s => s.FirstName);
                break;
            case "fname_desc":
                employees = employees.OrderByDescending(s => s.FirstName);
                break;
            case "lname_desc":
                employees = employees.OrderByDescending(s => s.LastName);
                break;
            case "deptname":
                employees = employees.OrderBy(s => s.Department.Name);
                break;
            case "deptname_desc":
                employees = employees.OrderByDescending(s => s.Department.Name);
                break;
            case "time_slot":
                employees = employees.OrderBy(s => s.Appointment.AppointmentID);
                break;
            case "time_slot_desc":
                employees = employees.OrderByDescending(s => s.Appointment.AppointmentID);
                break;
            default:
                employees = employees.OrderBy(s => s.LastName);
                break;
        }

        int pageSize = 10;
        return View(await PaginatedList<Employee>.CreateAsync(employees.AsNoTracking(), page ?? 1, pageSize));
    }

    // GET: Employees/Details/5
    public async Task<IActionResult> Details(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var employee = await _context.Employees
            .Include(e => e.Appointment)
            .Include(e => e.Department)
            .SingleOrDefaultAsync(m => m.EmployeeID == id);
        if (employee == null)
        {
            return NotFound();
        }

        return View(employee);
    }

    // GET: Employees/Confirmation/5
    public async Task<IActionResult> Confirmation(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var employee = await _context.Employees.Include(d => d.Department).Include(a => a.Appointment)
            .SingleOrDefaultAsync(m => m.EmployeeID == id);

        if (employee == null)
        {
            return NotFound();
        }

        return View(employee);
    }

    // GET: Employees/Create
    public IActionResult Create()
    {
        var departments = _context.Departments.ToList();
        var appointments = _context.Appointments.Include(x => x.Employees).Where(x => !x.Em‌​ployees.Any()).ToLis‌​t();

        var viewModel = new EmployeeFormVM
        {
            Departments = departments,
            Appointments = appointments
        };

        return View(viewModel);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(EmployeeFormVM employee)
    {
        if (ModelState.IsValid)
        {
            var emp = new Employee();
            {
                emp.FirstName = employee.FirstName;
                emp.LastName = employee.LastName;
                emp.DepartmentID = employee.DepartmentID;
                emp.AppointmentID = employee.AppointmentID;
            }

            // Query DB to check if Employee exists with same First/Last Name
            Employee existingEmployee = await _context.Employees.SingleOrDefaultAsync(m => m.FirstName == employee.FirstName && m.LastName == employee.LastName);
            if (existingEmployee != null)
            {
                // Display Error if duplicate employee
                ModelState.AddModelError(string.Empty, "An employee with this name has already registered. Please contact the Service Desk for any scheduling conflicts.");
                employee.Departments = _context.Departments.ToList();
                //employee.Appointments = _context.Appointments.ToList();
                employee.Appointments = _context.Appointments.ToList();
                return View(employee);
            }

            // Query DB to check if appointment has already been assigned to an employee
            Employee existingAppointment = await _context.Employees.SingleOrDefaultAsync(m => m.AppointmentID == employee.AppointmentID);
            if (existingAppointment != null)
            {
                // Display error if the appointment was already chosen
                ModelState.AddModelError(string.Empty, "This appointment has already been taken. Please select another timeslot.");
                employee.Departments = _context.Departments.ToList();
                //employee.Appointments = _context.Appointments.ToList();
                employee.Appointments = _context.Appointments.ToList();
                return View(employee);
            }

            _context.Add(emp);
            await _context.SaveChangesAsync();
            //return RedirectToAction(nameof(Index));

            var newlyCreatedId = emp.EmployeeID;
            return RedirectToAction(nameof(Confirmation), new { id = newlyCreatedId });
        }
        return View(employee);
    }

    // GET: Employees/Edit/5
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var employeevm = new EmployeeFormVM();
        {
            Employee employee = await _context.Employees.SingleOrDefaultAsync(m => m.EmployeeID == id);

            if (employee == null)
            {
                return NotFound();
            }

            employeevm.EmployeeID = employee.EmployeeID;
            employeevm.FirstName = employee.FirstName;
            employeevm.LastName = employee.LastName;

            // Retrieve list of Departments
            var departments = _context.Departments.ToList();
            employeevm.Departments = departments;
            // Set the selected department
            employeevm.DepartmentID = employee.DepartmentID;

            // Retrieve list of Appointments
            var appointments = _context.Appointments.ToList();
            employeevm.Appointments = appointments;
            // Set the selected department
            employeevm.AppointmentID = employee.AppointmentID;
        }
        return View(employeevm);
    }

    // POST: Employees/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(EmployeeFormVM vmEdit)
    {
        if (ModelState.IsValid)
        {
            Employee employee = _context.Employees.SingleOrDefault(e => e.EmployeeID == vmEdit.EmployeeID);

            if (employee == null)
            {
                return NotFound();
            }

            employee.FirstName = vmEdit.FirstName;
            employee.LastName = vmEdit.LastName;
            employee.DepartmentID = vmEdit.DepartmentID;
            employee.AppointmentID = vmEdit.AppointmentID;

            try
            {
                _context.Update(employee);
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!EmployeeExists(vmEdit.EmployeeID))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return RedirectToAction(nameof(Index));
        }
        return View(vmEdit);
    }

    // GET: Employees/Delete/5
    public async Task<IActionResult> Delete(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var employee = await _context.Employees
            .Include(e => e.Appointment)
            .Include(e => e.Department)
            .SingleOrDefaultAsync(m => m.EmployeeID == id);
        if (employee == null)
        {
            return NotFound();
        }

        return View(employee);
    }

    // POST: Employees/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteConfirmed(int id)
    {
        var employee = await _context.Employees.SingleOrDefaultAsync(m => m.EmployeeID == id);
        _context.Employees.Remove(employee);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }

    private bool EmployeeExists(int id)
    {
        return _context.Employees.Any(e => e.EmployeeID == id);
    }
}

Create View

@using (Html.BeginForm("Create", "Employees"))
    {
        @Html.ValidationSummary(true, "", new { @class = "validation-summary-errors" })

        <div class="form-group">
            @Html.LabelFor(e => e.FirstName)
            @Html.TextBoxFor(e => e.FirstName, new { @class = "form-control" })
            @Html.ValidationMessageFor(e => e.FirstName)
        </div>

        <div class="form-group">
            @Html.LabelFor(e => e.LastName)
            @Html.TextBoxFor(e => e.LastName, new { @class = "form-control" })
            @Html.ValidationMessageFor(e => e.LastName)
        </div>

        <div class="form-group">
            @Html.LabelFor(d => d.DepartmentID)
            @Html.DropDownListFor(d => d.DepartmentID, new SelectList(Model.Departments, "DepartmentID", "Name"), "", new { @class = "form-control" })
            @Html.ValidationMessageFor(d => d.DepartmentID)
        </div>

        <div class="form-group">
            @Html.LabelFor(a => a.AppointmentID)
            @Html.DropDownListFor(a => a.AppointmentID, new SelectList(Model.Appointments, "AppointmentID", "TimeSlot"), "", new { @class = "form-control" })
            @Html.ValidationMessageFor(a => a.AppointmentID)
        </div>

        <div class="form-group">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    }

Edit View

@using (Html.BeginForm("Edit", "Employees"))
    {
        <div class="form-group">
            @Html.LabelFor(e => e.FirstName)
            @Html.TextBoxFor(e => e.FirstName, new { @class = "form-control" })
            @Html.ValidationMessageFor(e => e.FirstName)
        </div>

        <div class="form-group">
            @Html.LabelFor(e => e.LastName)
            @Html.TextBoxFor(e => e.LastName, new { @class = "form-control" })
            @Html.ValidationMessageFor(e => e.LastName)
        </div>

        <div class="form-group">
            @Html.LabelFor(d => d.DepartmentID)
            @Html.DropDownListFor(d => d.DepartmentID, new SelectList(Model.Departments, "DepartmentID", "Name"), "", new { @class = "form-control" })
            @Html.ValidationMessageFor(d => d.DepartmentID)
        </div>

        <div class="form-group">
            @Html.LabelFor(a => a.AppointmentID)
            @Html.DropDownListFor(a => a.AppointmentID, new SelectList(Model.Appointments, "AppointmentID", "TimeSlot"), "", new { @class = "form-control" })
            @Html.ValidationMessageFor(a => a.AppointmentID)
        </div>

        @Html.HiddenFor(e => e.EmployeeID)

        <div class="form-group">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    }

Confirmation View

<div class="col-md-12">
    <img src="~/images/confirm.png" />
    <h2>Thank you @Html.DisplayFor(model => model.FirstName) @Html.DisplayFor(model => model.LastName)!</h2>    
    <p>Your <b>@Html.DisplayFor(model => model.Appointment.TimeSlot)</b> appointment has been booked. If you need to reschedule this appointment, please call the Service Desk at x1380.</p>
</div>

Solution

  • @Max is right. IDataProtectionProvider is what you want to look at.

    IDataProtectionProvider can be used on windows or unix.

    It's true it can't be used as a clientside javascript library but there are ways to still leverage it.

    The simplest approach is to encrypt the ID before sending the view's HTML to the browser and including the encrypted ID in an HTML data- attribute, etc, where your client javascript can get access to it to post back (or used in a query string if you so choose) with the edit or delete request.

    Below is an example of how to use the IDataProtectionProvider in a controller to encrypt and decrypt an id.

     public class HomeController : Controller{
         IDataProtector dataProtector;
        
         public HomeController(IDataProtectionProvider provider){
             dataProtector = provider.CreateProtector(GetType().FullName);
         }
        
        [HttpGet]
        public IActionResult Get() {
            int id = 1234;
    
            string encryptedId = dataProtector.Protect(id.ToString());
    
            int decryptedId = 0;
            if(int.TryParse(dataProtector.Unprotect(encryptedId), out decryptedId) == false){
                throw new Exception("Invalid cypher text");
            }
    
            //at this point decryptedId contains the decrypted value.
    
       }
        
    

    Please note that when this dataProtector is created above it uses GetType().FullName as the encryption "purpose". This is a common practice that appears in the .net core framework code. The "purpose" is used as additional context data when doing the encryption and is essentially used to derive a purpose specific subkey for encrypting/decrypting data. In this case since I set it to GetType().FullName it will be the fully qualified name of the controller. That's great if you are encrypting and decrypting from within the same controller, it will work no problem. BUT, if you want to encrypt in one controller and decrypt in a different controller (or any other class for that matter), then the important thing to know is that the purpose string passed in this line dataProtector = provider.CreateProtector(purpose); MUST be the same for the dataProtector used to decrypt as it was for the dataProtector used to encrypt. (i.e. it can't be the class name if the class used to decrypt is different than the one used to encrypt).

    Additionally, this is a article that you may find useful: https://www.mikesdotnetting.com/Article/295/encryption-and-decryption-in-asp-net-core