Search code examples
c#asp.net-corejwtasp.net-core-mvc

JWT in ASP.NET Core MVC


I have never worked with ASP.NET Core before. While I'm just messing around, I have a desire to do my authorization using JWT in ASP.NET Core MVC. As a result, I looked at several guides, read several articles, maybe I didn't understand something, but for some reason authorization does not work for me, although I receive a token, I immediately make a request to the controller with the Authorize attribute and receive the response

Bearer error= "invalid_token"

Receive token, then copy token and do post method, that require authorization

POST https://localhost:7081/Account/Login
Content-Type: application/json

{
    "UserName": "Admin",
    "Password": "aA12345!"
}

###
GET https://localhost:7081/Users/Index
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiQWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsIm5iZiI6MTcyMDk1MDc1NSwiZXhwIjoxNzIwOTU0MzU1LCJpc3MiOiJNeVN0ZWFtREJEYXRhYmFzZSIsImF1ZCI6Ik15U3RlYW1EQkRhdGFiYXNlIn0.lgXz-1z7CJwJiJsqjn_q7WMQy6-rZ_EvbYlEDh9JrGk

Controller code

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;

namespace MySteamDBMetacritic.Controllers
{
    public class AccountController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly ApplicationDbContext _context;
        private readonly UserManager<User> _userManager;
        private readonly SignInManager<User> _signInManager;
        private readonly IConfiguration _configuration;

        public AccountController(ILogger<HomeController> 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)
                {
                    _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);
                    }
                }
            }

            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 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),
                    new Claim(ClaimsIdentity.DefaultRoleClaimType, string.Join(',',_userManager.GetRolesAsync(user).Result))
                };

                ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token", 
                                  ClaimsIdentity.DefaultNameClaimType,
                                  ClaimsIdentity.DefaultRoleClaimType);

                return claimsIdentity;
            }

            return null;
        }
    }
}

I haven’t worked with EF before, it created its own tables, as I understand it, some are also used for authorization here, maybe the problem is that it doesn’t save anything there, but for some reason in all the guides, which I looked at, everything somehow works.

I think it’s not worth posting all code here, but if you suddenly need to look at something, here it is https://github.com/Drobovik04/MySteamDBMetacritic

Also, one more question - is there some simple mechanism for sending a token in requests, otherwise for some reason there are a lot of places where they attach JS to the view and are tied to pressing buttons on the form, then describe the request via fetch, I’m just very confused by this implementation, there is probably a more convenient and correct way

  • Tried: rewrite code

  • Expected: success authorization

  • Result: receiving token, saving it, but bearer error


Solution

  • After checking your repo, I found there are 2 issues in your project.

    1. I found the IDX1400 error by using OnAuthenticationFailed method, here is the sample for you.

    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = false,
            ValidateIssuerSigningKey = false,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        }; 
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";
                var result = Newtonsoft.Json.JsonConvert.SerializeObject(new { message = "Authentication failed" });
                return context.Response.WriteAsync(result);
            }
        };
    });
    

    Solution

    Add System.IdentityModel.Tokens.Jwt Package, and use it in your Token method.

    enter image description here

    2. window.location.href does not support Bearer Token. So the following is just my test code, just for reference. Not the soultion for window.location.href.

    @model LoginViewModel
    
    <h2>Вход в приложение</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>
    }
    

    enter image description here