JWT в ASP.NET Core MVC

Я никогда раньше не работал с ASP.NET Core. Пока я просто возюсь, у меня есть желание сделать свою авторизацию с помощью JWT в ASP.NET Core MVC. В итоге я посмотрел несколько гайдов, прочитал несколько статей, возможно я чего-то не понял, но у меня почему-то не работает авторизация, хотя токен я получаю, я сразу делаю запрос контролеру с Authorize атрибут и получить ответ

Ошибка носителя = "invalid_token"

Получите токен, затем скопируйте токен и выполните метод публикации, требующий авторизации.

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

Код контроллера

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;
        }
    }
}

Я раньше не работал с EF, он создавал свои таблицы, я так понимаю, некоторые здесь тоже используются для авторизации, возможно проблема в том, что он там ничего не сохраняет, но почему-то во всех гайдах, который я смотрел, все как-то работает.

Думаю, не стоит выкладывать сюда весь код, но если вдруг понадобится что-то посмотреть, вот оно https://github.com/Drobovik04/MySteamDBMetacritic

Также еще один вопрос - есть ли какой-то простой механизм отправки токена в запросах, а то почему-то много мест, где к представлению привязывают JS и завязывают на нажатие кнопок на форме, а затем описывают запрос через fetch, меня просто очень смущает такая реализация, наверное есть более удобный и правильный способ

  • Пробовал: переписать код

  • Ожидается: успешная авторизация

  • Результат: получение токена, сохранение его, но ошибка носителя.

Есть ли у вас какие-либо журналы, показывающие, почему токен недействителен, какие-либо дополнительные сведения? Пробовали ли вы отлаживать или проверять компоненты токена на jwt.io?

Prolog 14.07.2024 18:19

проверено, подпись действительна

CriticalError 14.07.2024 20:39

пробовал IncludeErrorDetails ?

Peko Miko 15.07.2024 07:21

проверил с помощью IncludeErrorDetails, но ничего не показывает, также попробовал выполнить аутентификацию на webapi, это сработало, но то же самое в mvc не работает почтальон ничего не показывает может быть есть что-то, что переопределяет какой-то код

CriticalError 20.07.2024 18:03

WebApi, который работал github.com/Drobovik04/WebApiTest, пробовал сделать как здесь в mvc, но не получилось или я допустил ошибку

CriticalError 20.07.2024 18:11
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
106
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Проверив ваш репозиторий, я обнаружил, что в вашем проекте есть две проблемы.

1. Я обнаружил ошибку IDX1400 с помощью метода OnAuthenticationFailed, вот вам образец.

.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);
        }
    };
});

Решение

Добавьте пакет System.IdentityModel.Tokens.Jwt и используйте его в своем методе Token.

2. window.location.href не поддерживает токен на предъявителя. Итак, ниже приведен мой тестовый код, просто для справки. Не душа для 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>
}

может я что-то не так сделал, но это не работает? ошибка

CriticalError 16.07.2024 20:16

Также, не могли бы вы дать мне совет в целом по разработке на asp net core, может быть я неправильно строю архитектуру приложения и ее можно лучше организовать, чтобы было проще работать? И еще, можно ли как-то автоматически включать токен во все запросы. То есть как только я залогинюсь, какой-то скрипт будет отправлять токен на все последующие запросы к серверу, даже если этот токен не нужен? Также, возможно, я что-то делаю не так, и в asp net core есть способы более удобно и красиво реализовать jwt авторизацию?

CriticalError 16.07.2024 20:24

Привет @CriticalError JWT очень распространен в проектах веб-API, поскольку он обеспечивает механизм аутентификации без сохранения состояния, подходящий для передачи пользовательской информации между клиентами и серверами.

Jason Pan 17.07.2024 07:29

@CriticalError Конечно, у нас также должна быть возможность добавлять токен jwt при отправке всех http-запросов, как и в других внешних и внутренних проектах. Возможно, вы сможете провести небольшое исследование о том, как работать с jwt в проекте webapi. Если вы просто хотите добавить аутентификацию в свой проект asp.net core mvc, вам следует сначала проверить официальный документ.

Jason Pan 17.07.2024 07:34

написал простой WebApi, все работает, попробовал сделать то же самое с моим проектом, но не помогло. WebApi: github.com/Drobovik04/WebApiTest. Скопируйте все, что участвует в аутентификации, и вставьте. Отредактированный исходный проект не был отправлен на github.

CriticalError 20.07.2024 18:16

отладчик говорит idx 14100, что точек нет, но точки в токене есть, я не понимаю 🤯

CriticalError 20.07.2024 19:04

После некоторых исследований я нашел это https://github.com/dotnet/aspnetcore/issues/52286. Похоже проблема была не у меня, а у .NET 8. Вам необходимо реализовать свой собственный TokenHandler.

Другие вопросы по теме