I use JWT
token, send a request to receive a token, receive a token, how can I make a redirection in a convenient form after successfully completing the authentication procedure? I tried to do this via js (the solution works on the login page, we get a token and a link to go to, make a request to fetch and embed the response in the page, I understand that this is garbage), I would like to know if there is a mechanism in ASP .NET Core MVC, which itself will embed a token in all requests if it is required, rather than having to write various js scripts everywhere for this. I also want to know if it is possible to somehow redirect to another link and at the same time obtain the token through one method, in my case the Login
method.
Login.cshtml
@model LoginViewModel
<h2>Login</h2>
<form id="loginForm" method="post" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@Model.ReturnUrl">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div>
<label asp-for="UserName"></label><br />
<input asp-for="UserName" />
<span asp-validation-for="UserName"></span>
</div>
<div>
<label asp-for="Password"></label><br />
<input asp-for="Password" />
<span asp-validation-for="Password"></span>
</div>
<div>
<label asp-for="RememberMe"></label><br />
<input asp-for="RememberMe" />
</div>
<div>
<input type="submit" value="Login" />
</div>
</form>
<div id="content"></div>
@section Scripts {
<script>
document.getElementById('loginForm').addEventListener('submit', async function (event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const username = formData.get('UserName');
const password = formData.get('Password');
const response = await fetch('/Account/Login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
UserName: username,
Password: password
})
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('token', data.token.access_token);
const returnUrl = form.getAttribute('asp-route-returnUrl');
var newUrl = returnUrl ? returnUrl : '/Users/Index';
loadPage(newUrl);
} else {
const errorText = await response.text();
alert('Login failed: ' + errorText);
}
});
async function loadPage(url) {
const token = localStorage.getItem('token');
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const content = await response.text();
document.getElementById('content').innerHTML = content;
} else {
alert('Failed to load the page: ' + response.statusText);
}
}
</script>
}
AccountController.cs
provides Registration, Login, Logout
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using MySteamDBMetacritic.Db;
using MySteamDBMetacritic.Models;
using MySteamDBMetacritic.ViewModels;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Runtime.Intrinsics.Arm;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace MySteamDBMetacritic.Controllers
{
public class AccountController : Controller
{
private readonly ILogger<AccountController> _logger;
private readonly ApplicationDbContext _context;
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly IConfiguration _configuration;
public AccountController(ILogger<AccountController> logger, ApplicationDbContext context, UserManager<User> userManager, SignInManager<User> signInManager, IConfiguration configuration)
{
_context = context;
_logger = logger;
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
}
[HttpGet]
public IActionResult Register()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Register([FromBody]RegisterViewModel model)
{
if (ModelState.IsValid)
{
User user = new User { Email = model.Email, UserName = model.UserName };
if (_userManager.Users.FirstOrDefault(x=>x.Email==user.Email) != default(User))
{
return View(model);
}
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
result = await _userManager.AddToRoleAsync(user, "User");
if (result.Succeeded)
{
_context.SaveChanges();
await _signInManager.SignInAsync(user, false);
return Json(new { token = Token(model.UserName, model.Password), returnUrl = Url.Action("Index", "Game") });
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
return View(model);
}
[HttpGet]
public IActionResult Login(string returnUrl = null)
{
return View(new LoginViewModel { ReturnUrl = returnUrl });
}
[HttpPost]
public async Task<IActionResult> Login([FromBody]LoginViewModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, false);
if (result.Succeeded)
{
if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return Json(new { token = ((JsonResult)Token(model.UserName, model.Password)).Value, returnUrl = model.ReturnUrl});
}
else
{
return Json(new { token = ((JsonResult)Token(model.UserName, model.Password)).Value, returnUrl = Url.Action("Index", "Game") });
}
}
else
{
ModelState.AddModelError("", "Incorrect username or password");
}
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Index", "Game");
}
public async Task<IActionResult> ChangePassword(int id)
{
User user = await _userManager.FindByIdAsync(id.ToString());
if (user == null)
{
return NotFound();
}
ChangePasswordViewModel model = new ChangePasswordViewModel { Id = user.Id, Email = user.Email };
return View(model);
}
[HttpPost]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (ModelState.IsValid)
{
User user = await _userManager.FindByIdAsync(model.Id.ToString());
if (user != null)
{
var _passwordValidator = HttpContext.RequestServices.GetService(typeof(IPasswordValidator<User>)) as IPasswordValidator<User>;
var _passwordHasher = HttpContext.RequestServices.GetService(typeof(IPasswordHasher<User>)) as IPasswordHasher<User>;
IdentityResult result = await _passwordValidator.ValidateAsync(_userManager, user, model.NewPassword);
if (result.Succeeded)
{
user.PasswordHash = _passwordHasher.HashPassword(user, model.NewPassword);
await _userManager.UpdateAsync(user);
return RedirectToAction("Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
else
{
ModelState.AddModelError(string.Empty, "User is missing");
}
}
return View(model);
}
public IActionResult Token(string username, string password)
{
var identity = GetIdentity(username, password).GetAwaiter().GetResult();
if (identity == null)
{
return BadRequest(new { errorText = "Invalid username or password." });
}
var now = DateTime.Now;
var jwt = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
//notBefore: now,
claims: identity.Claims,
//expires: now.Add(TimeSpan.FromMinutes(double.Parse(_configuration["Jwt:ExpiresMinutes"]))),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])), SecurityAlgorithms.HmacSha256));
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
access_token = encodedJwt,
username = identity.Name
};
return Json(response);
}
private async Task<ClaimsIdentity> GetIdentity(string username, string password)
{
User user = await _userManager.FindByNameAsync(username);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)
};
claims.AddRange(_userManager.GetRolesAsync(user).Result.Select(x => new Claim(ClaimsIdentity.DefaultRoleClaimType, x)));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
return claimsIdentity;
}
return null;
}
}
}
UsersController.cs
I decided to divide the functionality of the controller into two, in AccountController the user registers himself and can log in, in UserController the user with the admin role can change the password, create/delete users.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using MySteamDBMetacritic.Models;
using MySteamDBMetacritic.ViewModels;
using System.Security.Claims;
namespace MySteamDBMetacritic.Controllers
{
[Authorize(Roles = "Admin")]
public class UsersController : Controller
{
UserManager<User> _userManager;
public UsersController(UserManager<User> userManager)
{
_userManager = userManager;
}
public IActionResult Index()
{
var cl = HttpContext.User.Claims;
return View(_userManager.Users.ToList());
}
public IActionResult Create() => View();
[HttpPost]
public async Task<IActionResult> Create(CreateUserViewModel model)
{
if (ModelState.IsValid)
{
User user = new User { Email = model.Email, UserName = model.UserName };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
return View(model);
}
public async Task<IActionResult> Edit(int id)
{
User user = await _userManager.FindByIdAsync(id.ToString());
if (user == null)
{
return NotFound();
}
EditUserViewModel model = new EditUserViewModel { Id = user.Id, Email = user.Email, UserName = user.UserName };
return View(model);
}
[HttpPost]
public async Task<IActionResult> Edit(EditUserViewModel model)
{
if (ModelState.IsValid)
{
User user = await _userManager.FindByIdAsync(model.Id.ToString());
if (user != null)
{
user.Email = model.Email;
user.UserName = model.UserName;
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
}
return View(model);
}
[HttpPost]
public async Task<ActionResult> Delete(int id)
{
User user = await _userManager.FindByIdAsync(id.ToString());
if (user != null)
{
IdentityResult result = await _userManager.DeleteAsync(user);
}
return RedirectToAction("Index");
}
}
}
I’m also open to any advice on the application as a whole, maybe there are mistakes that should be avoided in the future. Link on my project.
I would like to know if there is a mechanism in ASP .NET Core MVC, which itself will embed a token in all requests if it is required, rather than having to write various js scripts everywhere for this.
Well, in order to set Authorization
request header while sending reuqest to API globally its quite easy if you use ajax request which has beforeSend attribute, where can set your token once then no where you don't need to do that.
Since fetch doesn't support beforeSend
—that's specific to jQuery's $.ajax
. In fetch, you need to include headers directly in the options object
to globally set the Authorization header for all fetch API requests in your JavaScript file.
You can create a wrapper function around the fetch API that automatically includes the token in the headers.
So, you should have funtion like below in your js script file and whereever, you need to pass the auth header, you can link the script and call the function and pass the request URL.
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem('token');
const headers = new Headers(options.headers || {});
if (token) {
headers.append('Authorization', `Bearer ${token}`);
}
const fetchOptions = {
...options,
headers: headers
};
try {
const response = await fetch(url, fetchOptions);
if (response.ok) {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else if (contentType && contentType.includes('text/html')) {
return await response.text();
} else {
throw new Error('Unsupported content type: ' + contentType);
}
} else {
if (response.status === 403) {
window.location.href = '/Users/AccessDenied';
} else {
throw new Error('Network response was not ok.');
}
}
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
Considering your scenari, you could call the function as following:
async function loadPage(url) {
const token = localStorage.getItem('token');
const content = await fetchWithAuth(url, { method: 'GET' });
document.getElementById('content').innerHTML = content;
}
UserController the user with the admin role can change the password, create/delete users.
Well, in that scenario, before executing creation and deletion, you could check the userRole. For instance, you could write [Authorize(Roles = "Admin")]
before your controller action. Same goes for other operation.
Apart from that, based on your other question, I have already explain and given you the guideline above. If you directly want to execute read-write operation within the controller then you could use designated role in Authorize
attribute.
Yeah, rest of your code seems okay.
Note: If you need additional details, please refer to this official document.