Skip to content

Commit

Permalink
implement identityserver4
Browse files Browse the repository at this point in the history
this took me three days to get it here but worth it

best practices from two of kevin dockx's pluralsight courses were utilized here to implement identityserver4:
- Securing Microservices in ASP.NET Core
- Securing ASP.NET Core 3 with OAuth2 and OpenID Connect

now, all authentication and token issuing is done by the identity microservice and not the client

since identityserver4 is meant to authenticate users only, i had to create a customer api with the microservice to deal with customers, making use of the customer service created earlier for this exact purpose

i also got to utilize a skill i never thought i'd utilize and that is custom authorization requirements at the level of the microservices. the custom authorization requirements add security in the middleware instead of having that logic being in the method which is an added bonus and a very handy technique

an issue i encountered was that the role wasn't mapped in the access token from identityserver4 so i created a custom x-user-role header that is sent with each relevant request with the administrator role which is checked for in the custom authorization requirements for the solution to work the way it always had

tokens issued now will never expire because of a helper library identitymodel.aspnetcore which deals with token refresh and setting a valid bearer token with each request, resolving a shortcoming of the previous implementation
  • Loading branch information
ShaylenReddy42 committed Sep 12, 2022
1 parent 2d1614a commit 6d1255c
Show file tree
Hide file tree
Showing 305 changed files with 95,924 additions and 373 deletions.
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ configure_file(
"${CMAKE_SOURCE_DIR}/cmake/SeelansTyres.Services.AddressService.AssemblyInfo.cs.in"
"${CMAKE_SOURCE_DIR}/Services/SeelansTyres.Services.AddressService/Properties/AssemblyInfo.cs")

configure_file(
"${CMAKE_SOURCE_DIR}/cmake/SeelansTyres.Services.IdentityService.AssemblyInfo.cs.in"
"${CMAKE_SOURCE_DIR}/Services/SeelansTyres.Services.IdentityService/Properties/AssemblyInfo.cs")

configure_file(
"${CMAKE_SOURCE_DIR}/cmake/SeelansTyres.Services.OrderService.AssemblyInfo.cs.in"
"${CMAKE_SOURCE_DIR}/Services/SeelansTyres.Services.OrderService/Properties/AssemblyInfo.cs")
Expand Down
130 changes: 36 additions & 94 deletions Frontend/SeelansTyres.Mvc/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -1,73 +1,60 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SeelansTyres.Mvc.Data.Entities;
using SeelansTyres.Mvc.Models;
using SeelansTyres.Mvc.Models.External;
using SeelansTyres.Mvc.Services;
using SeelansTyres.Mvc.ViewModels;
using System.Security.Cryptography;

namespace SeelansTyres.Mvc.Controllers;

public class AccountController : Controller
{
private readonly ILogger<AccountController> logger;
private readonly SignInManager<Customer> signInManager;
private readonly UserManager<Customer> userManager;
private readonly IAddressService addressService;
private readonly ICustomerService customerService;
private readonly IOrderService orderService;
private readonly IEmailService emailService;
private readonly ITokenService tokenService;

public AccountController(
ILogger<AccountController> logger,
SignInManager<Customer> signInManager,
UserManager<Customer> userManager,
IAddressService addressService,
ICustomerService customerService,
IOrderService orderService,
IEmailService emailService,
ITokenService tokenService)
IEmailService emailService)
{
this.logger = logger;
this.signInManager = signInManager;
this.userManager = userManager;
this.addressService = addressService;
this.customerService = customerService;
this.orderService = orderService;
this.emailService = emailService;
this.tokenService = tokenService;
}

[Authorize]
public async Task<IActionResult> Index()
{
var customer = await userManager.GetUserAsync(User);

var customerModel = new CustomerModel
{
Id = customer.Id,
FirstName = customer.FirstName,
LastName = customer.LastName,
Email = customer.Email,
PhoneNumber = customer.PhoneNumber
};
var customerId = Guid.Parse(User.Claims.Single(claim => claim.Type.EndsWith("nameidentifier")).Value);

var addresses = addressService.RetrieveAllAsync(customer.Id);
var orders = orderService.RetrieveAllAsync(customerId: customer.Id);
var customer = customerService.RetrieveSingleAsync(customerId);
var addresses = addressService.RetrieveAllAsync(customerId);
var orders = orderService.RetrieveAllAsync(customerId: customerId);

await Task.WhenAll(addresses, orders);
await Task.WhenAll(customer, addresses, orders);

var accountViewModel = new AccountViewModel
{
Customer = customerModel,
Customer = customer.Result,
Addresses = addresses.Result!,
Orders = orders.Result!
};

return View(accountViewModel);
}

[Authorize]
public IActionResult Login()
{
if (User.Identity!.IsAuthenticated)
Expand All @@ -78,44 +65,10 @@ public IActionResult Login()
return View();
}

[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
public async Task Logout()
{
if (ModelState.IsValid)
{
try
{
var result = await signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, false);

if (result.Succeeded)
{
var customer = await userManager.FindByEmailAsync(model.UserName);

tokenService.GenerateApiAuthToken(customer, await userManager.IsInRoleAsync(customer, "Administrator"));

return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError(string.Empty, "Login attempt failed!");
}
}
catch (InvalidOperationException ex)
{
logger.LogError(ex, "The database is unavailable");
ModelState.AddModelError(string.Empty, "Database is not connected, please try again later");
}
}

return View();
}

public async Task<IActionResult> Logout()
{
await signInManager.SignOutAsync();
HttpContext.Session.Remove("ApiAuthToken");

return RedirectToAction("Index", "Home");
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}

public IActionResult Register()
Expand All @@ -137,11 +90,7 @@ public async Task<IActionResult> Register(RegisterModel model)

if (succeeded is true && newCustomer is not null)
{
await signInManager.SignInAsync(newCustomer, isPersistent: false);

tokenService.GenerateApiAuthToken(newCustomer, await userManager.IsInRoleAsync(newCustomer, "Administrator"));

return RedirectToAction("Index", "Home");
return RedirectToAction(nameof(Login));
}
else
{
Expand All @@ -157,7 +106,7 @@ public async Task<IActionResult> UpdateAccount(AccountViewModel model)
{
await customerService.UpdateAsync(model.UpdateAccountModel);

return RedirectToAction("Index");
return RedirectToAction(nameof(Index));
}

[HttpPost]
Expand All @@ -167,11 +116,10 @@ public async Task<IActionResult> DeleteAccount(string password)

if (succeeded is true)
{
await signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
return RedirectToAction(nameof(Logout));
}

return RedirectToAction("Index");
return RedirectToAction(nameof(Index));
}

[HttpPost]
Expand All @@ -190,7 +138,7 @@ public async Task<IActionResult> AddNewAddress(AccountViewModel model)
ModelState.AddModelError(string.Empty, "API is unavailable to add your address,\nplease try again later");
}

return RedirectToAction("Index");
return RedirectToAction(nameof(Index));
}

[HttpPost]
Expand All @@ -200,7 +148,7 @@ public async Task<IActionResult> MarkAddressAsPreferred(Guid addressId)

_ = await addressService.MarkAddressAsPreferredAsync(customerId, addressId);

return RedirectToAction("Index");
return RedirectToAction(nameof(Index));
}

public IActionResult ResetPassword()
Expand All @@ -213,15 +161,17 @@ public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (model.SendCodeModel is not null)
{
var customer = await userManager.FindByEmailAsync(model.SendCodeModel.Email);
var customer = await customerService.RetrieveSingleAsync(model.SendCodeModel.Email);

if (customer is null)
{
ModelState.AddModelError(string.Empty, $"Customer with email {model.SendCodeModel.Email} does not exist!");
return View(model);
}

string token = await userManager.GeneratePasswordResetTokenAsync(customer);
string token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(256));

HttpContext.Session.SetString("ResetPasswordToken", token);

await emailService.SendResetPasswordTokenAsync(
email: model.SendCodeModel.Email,
Expand All @@ -238,28 +188,20 @@ await emailService.SendResetPasswordTokenAsync(
}
else if (model.ResetPasswordModel is not null)
{
var customer = await userManager.FindByEmailAsync(model.ResetPasswordModel.Email);
var customer = await customerService.RetrieveSingleAsync(model.ResetPasswordModel.Email);

var resetPasswordResult =
await userManager
.ResetPasswordAsync(
user: customer,
token: model.ResetPasswordModel.Token,
newPassword: model.ResetPasswordModel.Password);

if (resetPasswordResult.Succeeded is false)
if (model.ResetPasswordModel.Token != HttpContext.Session.GetString("ResetPasswordToken"))
{
foreach (var error in resetPasswordResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
ModelState.AddModelError(string.Empty, "Invalid token!");

return View(model);
}

await signInManager.PasswordSignInAsync(customer, model.ResetPasswordModel.Password, false, false);
HttpContext.Session.Remove("ResetPasswordToken");

return RedirectToAction("Index", "Home");
await customerService.ResetPasswordAsync(customer!.Id, model.ResetPasswordModel.Password);

return RedirectToAction(nameof(Login));
}

return View(model);
Expand Down
18 changes: 9 additions & 9 deletions Frontend/SeelansTyres.Mvc/Controllers/ShoppingController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using RazorLight;
using SeelansTyres.Mvc.Data.Entities;
using SeelansTyres.Mvc.Models;
using SeelansTyres.Mvc.Models.External;
using SeelansTyres.Mvc.Services;
Expand All @@ -16,24 +14,24 @@ public class ShoppingController : Controller
private readonly ILogger<ShoppingController> logger;
private readonly IAddressService addressService;
private readonly ICartService cartService;
private readonly ICustomerService customerService;
private readonly IOrderService orderService;
private readonly IEmailService emailService;
private readonly UserManager<Customer> userManager;

public ShoppingController(
ILogger<ShoppingController> logger,
IAddressService addressService,
ICartService cartService,
ICustomerService customerService,
IOrderService orderService,
IEmailService emailService,
UserManager<Customer> userManager)
IEmailService emailService)
{
this.logger = logger;
this.addressService = addressService;
this.cartService = cartService;
this.customerService = customerService;
this.orderService = orderService;
this.emailService = emailService;
this.userManager = userManager;
}

public async Task<IActionResult> Cart()
Expand Down Expand Up @@ -87,17 +85,19 @@ public async Task<IActionResult> Checkout()
{
var cartItems = cartService.Retrieve();

var customer = await userManager.GetUserAsync(User);
var customerId = Guid.Parse(User.Claims.Single(claim => claim.Type.EndsWith("nameidentifier")).Value);

var addresses = await addressService.RetrieveAllAsync(customer.Id);
var customer = await customerService.RetrieveSingleAsync(customerId);

var addresses = await addressService.RetrieveAllAsync(customerId);

var preferredAddress = addresses!.Single(address => address.PreferredAddress is true);

var order = new OrderModel
{
Id = 0,
OrderPlaced = DateTime.Now,
CustomerId = customer.Id.ToString(),
CustomerId = customerId.ToString(),
FirstName = customer.FirstName,
LastName = customer.LastName,
Email = customer.Email,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace SeelansTyres.Mvc.Models;
namespace SeelansTyres.Mvc.Models.External;

public class CustomerModel
{
Expand Down
9 changes: 9 additions & 0 deletions Frontend/SeelansTyres.Mvc/Models/External/PasswordModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;

namespace SeelansTyres.Mvc.Models;

public class PasswordModel
{
[Required]
public string Password { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;

namespace SeelansTyres.Mvc.Models;
namespace SeelansTyres.Mvc.Models.External;

public class RegisterModel
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;

namespace SeelansTyres.Mvc.Models;
namespace SeelansTyres.Mvc.Models.External;

public class UpdateAccountModel
{
Expand Down
Loading

0 comments on commit 6d1255c

Please sign in to comment.