GitHub – jeremyben/json-server-auth: Authentication & Authorization flow for JSON Server

Знакомство: что такое jwt?

В целях аутентификации JWT выступает в роли токена, выдаваемого сервером. Этот токен содержит относящееся к конкретному пользователю информационное наполнение в формате JSON. Клиенты могут использовать токен при взаимодействии с API (отправляя его как HTML-заголовок), чтобы API могли идентифицировать пользователя и выполнить соответствующее ему действие.

Но разве не может клиент просто создать случайное информационное наполнение и выдать себя за пользователя?

Хороший вопрос! Именно поэтому JWT также содержит подпись, которая создаётся сервером, выдавшим токен (предположим, конечной точкой вашей авторизации в системе). Любой другой сервер, получающий этот токен, может независимо проверить подпись, чтобы убедиться в подлинности информационного наполнения JSON и в том, что это наполнение было создано уполномоченным источником.

Но что если у меня есть действительный и подписанный JWT, а кто-то украдёт его из клиента? Смогут ли они постоянно использовать мой JWT?

Да. Если JWT будет украден, то вор сможет продолжать его использовать. API, принимающий JWT, выполняет самостоятельную проверку, не зависящую от его источника, следовательно сервер API никак не может знать о том, что этот токен был украден. Именно поэтому у JWT есть значение срока годности, которое намеренно создается коротким, и довольно часто его длительность определяется всего в 15 минут. Но при этом также нужно следить за тем, чтобы не произошло утечки JWT.

Из этих двух фактов формируются практически все особенности управления JWT. Т.е. мы стараемся избежать кражи, а если она всё же происходит, то нас спасает короткое время действия токенов.

Именно поэтому очень важно не хранить JWT на клиенте, например в куки или локальном хранилище. Поступая так, вы делаете своё приложение уязвимым для атак CSRF и XSS, которые при помощи вредоносных форм или скриптов могут использовать или украсть ваш токен, благоприятно размещённый в куки или локальном хранилище.

Есть ли у JWT какая-то конкретная структура?

В сериализованном виде JWT выглядит примерно так:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

Если вы раскодируете этот base64, то получите JSON в виде 3 важных частей: заголовка, информационного наполнения и подписи.

3 части JWT

Сериализованная форма будет иметь следующий формат:

[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [signature ]

JWT не зашифрован. Он закодирован в base64 и подписан. Поэтому любой может декодировать токен и использовать его данные. Подпись JWT используется для проверки легитимности источника этих данных.

Вот упрощённая схема, демонстрирующая, как JWT выдаётся (/login), а затем используется для совершения вызова API к другому сервису ( /api):

Рабочий процесс выдачи и использования JWT

Выглядит сложновато. Почему бы мне не придерживаться старых добрых токенов сессии?

Это уже наболевшая тема интернет-форумов. Мы коротко и категорично отвечаем, что бэкенд-разработчикам нравится использовать JWT по следующим причинам:

a) микросервисы;

b) отсутствие необходимости в централизованной базе данных токенов.

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

Именно поэтому разработчики API ценят JWT, а мы (с клиентской стороны) должны разобраться, как его использовать. Как бы то ни было, если вы можете обойтись токеном сессии, выданным вашим любимым монолитным фреймворком, то всё у вас в порядке и нет нужды в JWT.

Demo API: Managing devices (e.g. phones, computers)

For this blog post, we’ll create a simple Device Management API to list and add devices (e.g. phones, computers, and such) using two endpoints called devices/add and devices/listAll. The endpoints will be simple stub implementations, but we’ll still see how easy it is to create and manage endpoints.

Here’s an example JSON-RPC API request and its response to list all devices:

2. Usage

The usage is not very different from the example above. This time, however, the mapping is performed by the library:

That also means, of course, that invalid or missing parameters will be detected by the library and a JSON-RPC error is returned:

1. Sample code

Normally, a publicly available endpoint method must check the arguments that are passed to it. Code like this is not uncommon:

Instead of this annoying and polluting validation code, we can now define the constraints of each method argument using the @Validate annotation. This annotation is based on Symfony’s @Collection annotation and supports a variety of constraints.

To add validation support to your API server file (in our examples: api.php), all you need to do is wrap the existing JsonRpcEvaluator in a ValidatorEvaluator. After that, the API server logic looks like this (see example2/api.php):

2. Usage

In the case above, we passed a MonologLogger with a SyslogHandler to it, meaning that requests that we send will be logged to /var/log/syslog. The usage is the same as in the other examples, but we’ll see output like this when we tail syslog:

2. Using the API with JavaScript and cookie-based auth

I’ve been using curl in all the examples, but of course the primary use case of an API is often the use inside a web application. Since JSON-RPC requests are merely POST requests with JSON payload, you can use jQuery’s $.ajax() method — for instance like this:

In example 6 of the JSON-RPC demo project, we implement a small web page to display (and auto-refresh) the list of devices using the API, but only if we’re logged in via a cookie:

To achieve this, we implement yet another auth handler (CookieAuthHandler, see src/Api/Auth/CookieAuthHandler.php), and toggle a cookie when the “Login” button is pressed.

All that’s left is to call the API in regular intervals and update the UI. We implemented a small helper class called Datto.API.Client (see web/example6/js/client.js) to interact with the API from our web app:

That’s it. I hope this was helpful. Please let me know what you think in the comments!

I’d very much like to hear what you think of this post. Feel free to leave a comment. I usually respond within a day
or two, sometimes even faster. I will not share or publish your e-mail address anywhere.

Asp.net core | jwt-токены

Последнее обновление: 27.12.2022

Общие подходы к авторизации и аутентификации в ASP.NET Core Web API несколько отличаются от того, что мы имеем в MVC. В частности, в Web API механизм авторизации полагается преимущественно
на JWT-токены. Что такое JWT-токен?

JWT (или JSON Web Token) представляет собой веб-стандарт, который определяет способ передачи данных о пользователе в формате JSON в зашифрованном виде.

JWT-токен состоит из трех частей:

Для использования JWT-токенов создадим новый проект ASP.NET Core по типу Empty.

JWT Token in ASP.NET Core Web API

Далее добавим в проект папку Models, в которой определим новый класс Person:

public class Person
{
    public string Login { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}

Этот класс будет описывать учетные записи пользователей в приложении.

Для работы с JWT-токенами установим через Nuget пакет Microsoft.AspNetCore.Authentication.JwtBearer.

Также добавим в проект специальный класс AuthOptions, который будет описывать ряд свойств, нужных для генерации токена:

using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace TokenApp
{
    public class AuthOptions
    {
        public const string ISSUER = "MyAuthServer"; // издатель токена
        public const string AUDIENCE = "MyAuthClient"; // потребитель токена
        const string KEY = "mysupersecret_secretkey!123";   // ключ для шифрации
        public const int LIFETIME = 1; // время жизни токена - 1 минута
        public static SymmetricSecurityKey GetSymmetricSecurityKey()
        {
            return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY));
        }
    }
}

Константа ISSUER представляет издателя токена. Здесь можно определить любое название. AUDIENCE представляет потребителя токена – опять же может быть любая строка,
но в данном случае указан адрес текущего приложения.

Константа KEY хранит ключ, который будет применяться для создания токена.

Для встраивания функциональности JWT-токенов в конвейер обработки запроса используется компонент JwtBearerAuthenticationMiddleware.
Так, изменим класс Startup следующим образом:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

namespace TokenApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.RequireHttpsMetadata = false;
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            // укзывает, будет ли валидироваться издатель при валидации токена
                            ValidateIssuer = true,
                            // строка, представляющая издателя
                            ValidIssuer = AuthOptions.ISSUER,

                            // будет ли валидироваться потребитель токена
                            ValidateAudience = true,
                            // установка потребителя токена
                            ValidAudience = AuthOptions.AUDIENCE,
                            // будет ли валидироваться время существования
                            ValidateLifetime = true,

                            // установка ключа безопасности
                            IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
                            // валидация ключа безопасности
                            ValidateIssuerSigningKey = true,
                        };
                    });
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseDeveloperExceptionPage();

            app.UseDefaultFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }
}

Для установки аутентификации с помощью токенов в методе ConfigureServices в вызов services.AddAuthentication передается значение JwtBearerDefaults.AuthenticationScheme. Далее с помощью метода AddJwtBearer() добавляется конфигурация токена.

Для конфигурации токена применяется объект JwtBearerOptions, который позволяет с помощью свойств настроить работу с токеном. В данном случае использованы следующие свойства:

  • RequireHttpsMetadata: если равно false, то SSL при отправке токена не используется. Однако данный вариант установлен
    только дя тестирования. В реальном приложении все же лучше использовать передачу данных по протоколу https.

  • TokenValidationParameters: параметры валидации токена – сложный объект, определяющий, как токен будет валидироваться. Этот объект
    в свою очередь имеет множество свойств, которые позволяют настроить различные аспекты валидации токена. Но наиболее важные свойства:
    IssuerSigningKey – ключ безопасности, которым подписывается токен, и ValidateIssuerSigningKey – надо ли валидировать ключ безопасности.
    Ну и кроме того, можно установить ряд других свойств, таких как нужно ли валидировать издателя и потребителя токена, срок жизни токена, можно установить
    название claims для ролей и логинов пользователя и т.д.

Теперь мы можем использовать авторизацию на основе токенов. Однако в прокте пока не предусмотрена генерация токенов. По умолчанию в ASP.NET Core отсутствуют встроенные
возможности для создания токена. И в данном случае мы можем либо воспользоваться сторонними решениями (например,
IdentityServer или
OpenIdDict), либо же создать свой механизм. Выберем второй способ.

Создадим в проекте новую папку Controllers и добавим в нее новый контроллер AccountController:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using TokenApp.Models; // класс Person

namespace TokenApp.Controllers
{
    public class AccountController : Controller
    {
        // тестовые данные вместо использования базы данных
        private List<Person> people = new List<Person>
        {
            new Person {Login="[email protected]", Password="12345", Role = "admin" },
            new Person { Login="[email protected]", Password="55555", Role = "user" }
        };

        [HttpPost("/token")]
        public IActionResult Token(string username, string password)
        {
            var identity = GetIdentity(username, password);
            if (identity == null)
            {
                return BadRequest(new { errorText = "Invalid username or password." });
            }

            var now = DateTime.UtcNow;
            // создаем JWT-токен
            var jwt = new JwtSecurityToken(
                    issuer: AuthOptions.ISSUER,
                    audience: AuthOptions.AUDIENCE,
                    notBefore: now,
                    claims: identity.Claims,
                    expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)),
                    signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            var response = new
            {
                access_token = encodedJwt,
                username = identity.Name
            };

            return Json(response);
        }

        private ClaimsIdentity GetIdentity(string username, string password)
        {
            Person person = people.FirstOrDefault(x => x.Login == username && x.Password == password);
            if (person != null)
            {
                var claims = new List<Claim>
                {
                    new Claim(ClaimsIdentity.DefaultNameClaimType, person.Login),
                    new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role)
                };
                ClaimsIdentity claimsIdentity =
                new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType,
                    ClaimsIdentity.DefaultRoleClaimType);
                return claimsIdentity;
            }

            // если пользователя не найдено
            return null;
        }
    }
}

Для упрощения ситуации данные пользователей определены в виде простого списка. Для поиска пользователя в этом списке по логину и паролю определен метод
GetIdentity(), который возвращает объект ClaimsIdentity.

Принцип создания ClaimsIdentity здесь тот же, что и при аутентификации с помощью кук: создается набор объектов Claim, которые включают различные данные о пользователе,
например, логин, роль и т.д.

Для обработки запроса в контроллере создан метод Token, который сопоставлен с маршрутом “/token”. Этот метод обрабатывает запросы POST и через
параметры принимает логин и пароль пользователя.

Сам токен представляет объект JwtSecurityToken, для инициализации которого применяются все те же константы и ключ безопасности,
которые определены в классе AuthOptions и которые использовались в классе Startup для настройки JwtBearerAuthenticationMiddleware. Важно, чтобы эти значения совпадали.

С помощью параметра claims: identity.Claims в токен добавляются набор объектов Claim, которые содержат информацию о логине и роли пользователя.

Далее посредством метода JwtSecurityTokenHandler().WriteToken(jwt) создается Json-представление токена. И в конце он сериализуется и отправляет клиенту с помощью метода Json().

Таким образом генерируется токен.

Для тестирования токена создадим простенький контроллер ValuesController:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace TokenApp.Controllers
{
	[ApiController]
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        [Authorize]
        [Route("getlogin")]
        public IActionResult GetLogin()
        {
            return Ok($"Ваш логин: {User.Identity.Name}");
        }
		
        [Authorize(Roles = "admin")]
        [Route("getrole")]
        public IActionResult GetRole()
        {
            return Ok("Ваша роль: администратор");
        }
    }
}

И в конце добавим в проект для статических файлов папку wwwroot, а в нее – новый файл index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>JWT в ASP.NET Core Web API</title>
</head>
<body>
    <div id="userInfo" style="display:none;">
        <p>Вы вошли как: <span id="userName"></span></p>
        <input type="button" value="Выйти" id="logOut" />
    </div>
    <div id="loginForm">
        <h3>Вход на сайт</h3>
        <label>Введите email</label><br />
        <input type="email" id="emailLogin" /> <br /><br />
        <label>Введите пароль</label><br />
        <input type="password" id="passwordLogin" /><br /><br />
        <input type="submit" id="submitLogin" value="Логин" />
    </div>
    <div>
        <input type="submit" id="getDataByLogin" value="Данные по логину" />
    </div>
    <div>
        <input type="submit" id="getDataByRole" value="Данные по роли" />
    </div>

    <script>
        var tokenKey = "accessToken";

        // отпавка запроса к контроллеру AccountController для получения токена
        async function getTokenAsync() {

            // получаем данные формы и фомируем объект для отправки
            const formData = new FormData();
            formData.append("grant_type", "password");
            formData.append("username", document.getElementById("emailLogin").value);
            formData.append("password", document.getElementById("passwordLogin").value);

            // отправляет запрос и получаем ответ
            const response = await fetch("/token", {
                method: "POST",
                headers: {"Accept": "application/json"},
                body: formData
            });
            // получаем данные 
            const data = await response.json();

            // если запрос прошел нормально
            if (response.ok === true) {

                // изменяем содержимое и видимость блоков на странице
                document.getElementById("userName").innerText = data.username;
                document.getElementById("userInfo").style.display = "block";
                document.getElementById("loginForm").style.display = "none";
                // сохраняем в хранилище sessionStorage токен доступа
                sessionStorage.setItem(tokenKey, data.access_token);
                console.log(data.access_token);
             }
            else {
                // если произошла ошибка, из errorText получаем текст ошибки
                console.log("Error: ", response.status, data.errorText);
            }
        };
        // отправка запроса к контроллеру ValuesController
        async function getData(url) {
            const token = sessionStorage.getItem(tokenKey);

            const response = await fetch(url, {
                method: "GET",
                headers: {
                    "Accept": "application/json",
                    "Authorization": "Bearer "   token  // передача токена в заголовке
                }
            });
            if (response.ok === true) {
                
                const data = await response.json();
                alert(data)
            }
            else
                console.log("Status: ", response.status);
        };

        // получаем токен
        document.getElementById("submitLogin").addEventListener("click", e => {

            e.preventDefault();
            getTokenAsync();
        });

        // условный выход - просто удаляем токен и меняем видимость блоков
        document.getElementById("logOut").addEventListener("click", e => {

            e.preventDefault();
            document.getElementById("userName").innerText = "";
            document.getElementById("userInfo").style.display = "none";
            document.getElementById("loginForm").style.display = "block";
            sessionStorage.removeItem(tokenKey);
        });


        // кнопка получения имя пользователя  - /api/values/getlogin
        document.getElementById("getDataByLogin").addEventListener("click", e => {

            e.preventDefault();
            getData("/api/values/getlogin");
        });

        // кнопка получения роли  - /api/values/getrole
        document.getElementById("getDataByRole").addEventListener("click", e => {

            e.preventDefault();
            getData("/api/values/getrole");
        });
    </script>
</body>
</html>

Первый блок на странице выводит информацию о вошедшем пользователе и ссылку для выхода. Второй блок содержит форму для логина.

После нажатия кнопки на форме логина запрос будет отправляться методом POST на адрес “/token”. Поскольку за обработку запросов по этому маршруту
отвечает метод Token контроллера AccountController, по результатам работы которого будет формироваться токен.

Ответом сервера в случае удачной аутентификации будет примерно следующий объект:

{
	access_token : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93
					cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoicXdlcnR5IiwiaHR0cDovL3NjaGVtYXMub
					Wljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoidXNlciIsIm5iZi
					I6MTQ4MTYzOTMxMSwiZXhwIjoxNDgxNjM5MzcxLCJpc3MiOiJNeUF1dGhTZXJ2ZXIiLCJhdWQiOiJ
					odHRwOi8vbG9jYWxob3N0OjUxODg0LyJ9.dQJF6pALUZW3wGBANy_tCwk5_NR0TVBwgnxRbblp5Ho",
	username: "[email protected]"
}

Параметр access_token как раз и будет представлять токен доступа. Также в объекте передается дополнительная информация о нике
пользователя.

Для того, чтобы в коде js данный токен в дальнейшем был доступен, то он сохраняется в хранилище sessionStorage.

Последние два блока предназначены для отправки запросов к методам ValuesController. Чтобы отправить токен в запросе, нам нужно настроить в запросе заголовок Authorization:

headers: {
	"Accept": "application/json",
	"Authorization": "Bearer "   token  // передача токена в заголовке
}

В итоге весь проект будет выглядеть следующим образом:

JWT в ASP.NET Core Web API

По ранее сохраненному ключу получаем из хранилища sessionStorage токен и формируем заголовок.

Теперь после получения токена мы можем осуществить запрос к методам контроллера ValuesController:

Роли в токене JWT в ASP.NET Core Web API

В то же время если мы попробуем обратиться к тем же методам без токена или с токеном с истекшим сроком, то получим ошибку 401 (Unauthorized).

НазадСодержаниеВперед

Authorization flow 🛡️

JSON Server Auth provides generic guards as route middlewares.

To handle common use cases, JSON Server Auth draws inspiration from Unix filesystem permissions, especialy the numeric notation.

  • We add 4 for read permission.
  • We add 2 for write permission.

Of course CRUD is not a filesystem, so we don’t add 1 for execute permission.

Create asp.net core web api using visual studio 2022

We can create an API application with ASP.NET Core Web API template.

We have added a database connection string and also added valid audience, valid issuer and secret key for JWT authentication in above settings file.

Create an “ApplicationUser” class inside a new folder “Authentication” which will inherit the IdentityUser class. IdentityUser class is a part of Microsoft Identity framework. We will create all the authentication related files inside the “Authentication” folder.

ApplicationUser.cs

We can create the “ApplicationDbContext” class and add below code.

ApplicationDbContext.cs

Create a static class “UserRoles” and add below values.

UserRoles.cs

We have added two constant values “Admin” and “User” as roles. You can add many roles as you wish.

Create class “RegisterModel” for new user registration.

RegisterModel.cs

Create class “LoginModel” for user login.

LoginModel.cs

We can create a class “Response” for returning the response value after user registration and user login. It will also return error messages, if the request fails.

Response.cs

We can create an API controller “AuthenticateController” inside the “Controllers” folder and add below code.

AuthenticateController.cs

  1. using JWTAuthentication.Authentication;  
  2. using Microsoft.AspNetCore.Http;  
  3. using Microsoft.AspNetCore.Identity;  
  4. using Microsoft.AspNetCore.Mvc;  
  5. using Microsoft.Extensions.Configuration;  
  6. using Microsoft.IdentityModel.Tokens;  
  7. using System;  
  8. using System.Collections.Generic;  
  9. using System.IdentityModel.Tokens.Jwt;  
  10. using System.Security.Claims;  
  11. using System.Text;  
  12. using System.Threading.Tasks;  
  13.   
  14. namespace JWTAuthentication.Controllers  
  15. {  
  16.     [Route(“api/[controller]”)]  
  17.     [ApiController]  
  18.     public class AuthenticateController : ControllerBase  
  19.     {  
  20.         private readonly UserManager<ApplicationUser> userManager;  
  21.         private readonly RoleManager<IdentityRole> roleManager;  
  22.         private readonly IConfiguration _configuration;  
  23.   
  24.         public AuthenticateController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration)  
  25.         {  
  26.             this.userManager = userManager;  
  27.             this.roleManager = roleManager;  
  28.             _configuration = configuration;  
  29.         }  
  30.   
  31.         [HttpPost]  
  32.         [Route(“login”)]  
  33.         public async Task<IActionResult> Login([FromBody] LoginModel model)  
  34.         {  
  35.             var user = await userManager.FindByNameAsync(model.Username);  
  36.             if (user != null && await userManager.CheckPasswordAsync(user, model.Password))  
  37.             {  
  38.                 var userRoles = await userManager.GetRolesAsync(user);  
  39.   
  40.                 var authClaims = new List<Claim>  
  41.                 {  
  42.                     new Claim(ClaimTypes.Name, user.UserName),  
  43.                     new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),  
  44.                 };  
  45.   
  46.                 foreach (var userRole in userRoles)  
  47.                 {  
  48.                     authClaims.Add(new Claim(ClaimTypes.Role, userRole));  
  49.                 }  
  50.   
  51.                 var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration[“JWT:Secret”]));  
  52.   
  53.                 var token = new JwtSecurityToken(  
  54.                     issuer: _configuration[“JWT:ValidIssuer”],  
  55.                     audience: _configuration[“JWT:ValidAudience”],  
  56.                     expires: DateTime.Now.AddHours(3),  
  57.                     claims: authClaims,  
  58.                     signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)  
  59.                     );  
  60.   
  61.                 return Ok(new  
  62.                 {  
  63.                     token = new JwtSecurityTokenHandler().WriteToken(token),  
  64.                     expiration = token.ValidTo  
  65.                 });  
  66.             }  
  67.             return Unauthorized();  
  68.         }  
  69.   
  70.         [HttpPost]  
  71.         [Route(“register”)]  
  72.         public async Task<IActionResult> Register([FromBody] RegisterModel model)  
  73.         {  
  74.             var userExists = await userManager.FindByNameAsync(model.Username);  
  75.             if (userExists != null)  
  76.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User already exists!” });  
  77.   
  78.             ApplicationUser user = new ApplicationUser()  
  79.             {  
  80.                 Email = model.Email,  
  81.                 SecurityStamp = Guid.NewGuid().ToString(),  
  82.                 UserName = model.Username  
  83.             };  
  84.             var result = await userManager.CreateAsync(user, model.Password);  
  85.             if (!result.Succeeded)  
  86.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User creation failed! Please check user details and try again.” });  
  87.   
  88.             return Ok(new Response { Status = “Success”, Message = “User created successfully!” });  
  89.         }  
  90.   
  91.         [HttpPost]  
  92.         [Route(“register-admin”)]  
  93.         public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model)  
  94.         {  
  95.             var userExists = await userManager.FindByNameAsync(model.Username);  
  96.             if (userExists != null)  
  97.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User already exists!” });  
  98.   
  99.             ApplicationUser user = new ApplicationUser()  
  100.             {  
  101.                 Email = model.Email,  
  102.                 SecurityStamp = Guid.NewGuid().ToString(),  
  103.                 UserName = model.Username  
  104.             };  
  105.             var result = await userManager.CreateAsync(user, model.Password);  
  106.             if (!result.Succeeded)  
  107.                 return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = “Error”, Message = “User creation failed! Please check user details and try again.” });  
  108.   
  109.             if (!await roleManager.RoleExistsAsync(UserRoles.Admin))  
  110.                 await roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));  
  111.             if (!await roleManager.RoleExistsAsync(UserRoles.User))  
  112.                 await roleManager.CreateAsync(new IdentityRole(UserRoles.User));  
  113.   
  114.             if (await roleManager.RoleExistsAsync(UserRoles.Admin))  
  115.             {  
  116.                 await userManager.AddToRoleAsync(user, UserRoles.Admin);  
  117.             }  
  118.   
  119.             return Ok(new Response { Status = “Success”, Message = “User created successfully!” });  
  120.         }  
  121.     }  
  122. }  

We have added three methods “login”, “register”, and “register-admin” inside the controller class. Register and register-admin are almost same but the register-admin method will be used to create a user with admin role. In login method, we have returned a JWT token after successful login.

We can make below changes in “ConfigureServices” and “Configure” methods in “Startup” class as well.

Startup.cs

We can add “Authorize” attribute inside the “WeatherForecast” controller.

Generate json web token

The code below will check if the email exists and if the password matches what is in the database. We used the built-in password_verify() function to do the matching.

If login is valid, it will generate the JSON Web Token.

Replace // generate jwt will be here comment of login.php file with the following code.

// check if email exists and if password is correct
if($email_exists && password_verify($data->password, $user->password)){

    $token = array(
       "iat" => $issued_at,
       "exp" => $expiration_time,
       "iss" => $issuer,
       "data" => array(
           "id" => $user->id,
           "firstname" => $user->firstname,
           "lastname" => $user->lastname,
           "email" => $user->email
       )
    );

    // set response code
    http_response_code(200);

    // generate jwt
    $jwt = JWT::encode($token, $key);
    echo json_encode(
            array(
                "message" => "Successful login.",
                "jwt" => $jwt
            )
        );

}

// login failed will be here

Trigger when sign up form is submitted

We need to process the form data when it is submitted.

Replace the trigger when the registration form is submitted here comment of index.html file with the following code.

// trigger when registration form is submitted
$(document).on('submit', '#sign_up_form', function(){

	// get form data
	var sign_up_form=$(this);
	var form_data=JSON.stringify(sign_up_form.serializeObject());

	// submit form data to api
	$.ajax({
		url: "api/create_user.php",
		type : "POST",
		contentType : 'application/json',
		data : form_data,
		success : function(result) {
			// if response is a success, tell the user it was a successful sign up & empty the input boxes
			$('#response').html("<div class='alert alert-success'>Successful sign up. Please login.</div>");
			sign_up_form.find('input').val('');
		},
		error: function(xhr, resp, text){
			// on error, tell the user sign up failed
			$('#response').html("<div class='alert alert-danger'>Unable to sign up. Please contact admin.</div>");
		}
	});

	return false;
});

What is jwt?

In technical terms, JSON Web Token or JWT is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

You may read more here and here.

Вход в систему

Теперь, когда у нас есть базовое понимание JWT, давайте создадим простой процесс входа в систему и извлечём JWT. Вот что у нас должно получиться:

Процесс входа в систему для получения JWT

С чего же начать?

Процесс входа в систему принципиально не отличается от того, с которым вы сталкиваетесь регулярно. Например, вот форма авторизации, которая отправляет имя пользователя/пароль в конечную точку аутентификации и получает в ответ JWT-токен. Это может быть авторизация с помощью внешнего провайдера, шаг OAuth или OAuth2. Главное, чтобы в ответ на завершающий шаг входа в систему клиент получил JWT-токен.

Сначала мы создадим простую форму авторизации для отправки имени пользователя и пароля серверу авторизации. Сервер выдаст JWT-токен, и мы будет хранить его в памяти. В текущем руководстве мы не будем фокусироваться на сервере аутентификации в бэкенд, но вы запросто можете проверить его в примере репозитория для этой статьи.

Вот как может выглядеть обработчик handleSubmit для кнопки входа в систему:

Выход из системы

При использовании JWT “logout” просто стирает токен на стороне клиента, после чего он уже не может быть использован в последующих вызовах API.

Значит вызова API /logout не существует?

Конечная точка logout, по сути, не требуется, т.к. любой микросервиc, принимающий ваш JWT, будет продолжать его принимать. Если ваш сервер аутентификации удаляет JWT, то это не будет иметь значения, поскольку другие сервисы будут продолжать его принимать (поскольку весь смысл JWT в отсутствии централизованной координации).

Что делать, если мне нужно обеспечить невозможность продолжения использования токена?

Поэтому важно определять краткосрочные значения срока действия JWT. По этой же причине нам ещё более важно обеспечить таким образом защиту JWT от кражи. Токен действителен (даже после его удаления на клиенте), но только в течение короткого промежутка времени, что снижает вероятность его использования злоумышленниками.

Дополнительно вы можете добавить процесс обработки чёрных списков. В данном случае можно создать вызов API /logout, и ваш сервер аутентификации будет помещать токены в “недействительный список”. Как бы то ни было, все API сервисы, использующие JWT, теперь должны добавить дополнительный шаг к их верификации, чтобы проверять централизованный “чёрный список”. Это снова вводит центральное состояние и возвращает нас к тому, что мы имели до использования JWT.

Разве наличие чёрного списка не исключает преимущество JWT, гласящее о необязательности центрального хранилища?

В некотором смысле так и есть. Это опциональная предосторожность, к которой вы можете прибегнуть, если беспокоитесь, что ваш токен могут украсть. Но при этом она также повышает число необходимых верификаций.

Что произойдёт, если я авторизован на нескольких вкладках?

Один из способов разрешения этой ситуации заключается во введении в локальном хранилище глобального слушателя событий. При каждом обновлении этого ключа logout в локальном хранилище для одной вкладки, слушатель сработает и на других вкладках, также запустив ‘logout’ и перенаправив пользователей на экран входа в систему.

window.addEventListener('storage', this.syncLogout) 

//....

syncLogout (event) {
if (event.key === 'logout') {
console.log('logged out from storage!')
Router.push('/login')
}
}

Теперь при выходе нам нужно совершить два действия:

  1. обнулить токен;
  2. установить в локальном хранилище элемент logout.

Обработка

С обработкой вышла осечка. Я ламер-эникейщик и для разработки использую XAMPP (под Win10), который как то раз настроил и забыл. И вот что то там такое настроено, что я GET запросы в PHP-скрипт получаю как нормальный человек, а POST-запросы, только через одно место.

Но GET — не камильфо, потому что GET кэшуруется, а у нас не факт что ответ сервера будет точно таким как в прошлый раз, особенно когда ты ведёшь разработку, и серверная логика меняется каждые пять минут.

Поэтому пришлось смириться с использованием чёрной магии в виде file_get_contents(«php://input»), потому что, что бы я ни делал, но var_export($POST) стабильно выдавал «array()». Между прочим, буду благодарен за серию пинков в верном направлении.

А дальше всё просто:

$data = json_decode($rawData, true);

$rawPosition = $data['position'];
$rawCounterparty = $data['counterparty'];
$rawOrganization = $data['organization'];

Разобрали входные данные.

Дёрнули Юридическое лицо и контрагента:

const FIRST_INDEX = 0;
$counterpartyId = $counterpartyIdCollection[FIRST_INDEX];
$organizationId = $organizationIdCollection[FIRST_INDEX];

Сформировали поля запроса:

Отображение серверной части (ssr)

В отображении серверной стороны при работе с JWT-токенами есть дополнительные трудности.

Вот что нам нужно:

  1. Браузер делает запрос к URL приложения.
  2. SSR-сервер отображает страницу на основе идентичности пользователя.
  3. Пользователь получает отображённую страницу, а затем продолжает использовать приложение в качестве SPA (одностраничного приложения).

Откуда SSR сервер узнаёт, авторизован ли пользователь?

Браузер должен отправить SSR-серверу информацию о текущей идентичности пользователя. Единственный способ сделать это — использовать куки.

Мы уже реализовали рабочий процесс токена обновления через куки, поэтому когда мы делаем запрос к SSR-серверу, нам нужно убедиться, что токен обновления будет также отправлен.

Внимание! Для SSR на аутентифицированных страницах очень важно, чтобы домен API аутентификации (и, следовательно, домен куки refresh_token) совпадал с доменом SSR-сервера. В противном случае наши куки не смогут быть отправлены SSR-серверу.

Вот что делает SSR-сервер:

  1. При получении запроса на отображение конкретной страницы он захватывает куки refresh_token.
  2. Далее сервер использует эту куки, чтобы получить новый JWT для пользователя.
  3. Затем сервер использует этот новый токен и совершает все аутентифицированные запросы GraphQL для извлечения правильных данных.

Может ли пользователь продолжить выполнение аутентифицированных API-запросов, когда SSR-страница будет загружена?

К сожалению, без дополнительных манипуляций сделать это не получится. Как только SSR-сервер возвращает отображённый HTML, единственной оставшейся в браузере идентификацией пользователя является старая куки токена обновления, которая уже была использована SSR-сервером.

Если код нашего приложения попытается использовать этот токен обновления для запроса нового JWT, этот запрос провалится, и пользователь будет выброшен из системы.

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

Весь поток SSR от начала до конца:

Первое

Первое что надо сделать, это познакомиться с документацией. Познакомился.


Второе — составить план. Составил.

План, начало.

Действие первое — авторизация.


Действие второе — показать список Номенклатур.

Действие третье — добавить Заказ покупателя.

Действие четвёртое — добавить Позиции в Заказ покупателя.


Цель достигнута, конец плана.

Поехали!

Первым делом конечно гуглить, нагуглилась только

, туториалов, примеров — ноль.

Ещё нагуглилось: «JSON API доступен для подписчиков на всех тарифах, кроме Бесплатного» уупс! Платного мне конечно ни кто не дал, покупать не камильфо, но я подумал что если дали такое задание, то наверное на Бесплатном что то там функционирует и продолжил работу.

И конечно нагуглилось «moysklad-client — npm — JavaScript клиент для комфортной работы с API сервиса МойСклад», но я с JS исключительно на «Вы», и по условиям задания, написать надо на PHP. Так что даже разбираться не стал, что там на JS можно делать.

Постоянные сессии

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

Вы заметите, что если пользователи закрывают приложение и затем снова его открывают (предположим, закрывая и вновь открывая вкладку браузера), то им будет снова предложено войти в систему.

Приложения обычно спрашивают пользователей, хотят ли те “stay logged in” (оставаться авторизованными) в течение нескольких сессий или сохраняют их авторизованными по умолчанию. Именно это хотим реализовать и мы.

На данный момент мы не можем этого сделать, т.к. JWT находится в памяти и не сохраняется. Ранее мы уже рассматривали, почему не можем хранить JWT непосредственно в куки или локальном хранилище.

Как же можно безопасно сохранять сессии?

Как и говорилось ранее, мы можем сохранять токены обновления и затем использовать их для фонового обновления (т.е. обновлять наш краткосрочные JWT-токены, не прося пользователей авторизовываться повторно). При этом мы также можем использовать их для запроса нового JWT-токена для новой сессии.

Давайте предположим, что пользователь вышел из текущей сессии, закрыв вкладку браузера. Теперь давайте пронаблюдаем процесс, происходящий при последующем посещении пользователем приложения:

  1. Если мы видим, что в памяти у нас нет JWT, тогда мы запускаем процесс фонового обновления.
  2. Если токен обновления по-прежнему действителен (или не был пересоздан), тогда мы получаем новый JWT и можем продолжать.

Возможный случай ошибки:

В случае если наш срок действия токена обновления истечёт (предположим, пользователь вернётся в приложение через большой промежуток времени) или будет пересоздан (к примеру, из-за принудительного выхода), тогда клиент получит ошибку 401 для неавторизованного refresh_token.

Ещё в одном случае у нас может просто изначально не быть refresh_token, что также приведёт к ошибке, полученной от конечной точки /refresh_token, в результате которой пользователь будет перенаправлен на экран авторизации.

Вот пример кода, показывающий, как мы можем обработать эти ошибки при помощи logoutLink:

const logoutLink = onError(({ networkError }) => {
if (networkError.statusCode === 401) logout();
})

Принудительный выход или выход из всех сессий на всех устройствах

Теперь, когда пользователи авторизованы постоянно и будут оставаться в системе из сессии в сессию, появляется новая проблема, требующая решения: принудительный выход или выход из всех сессий на всех устройствах.

Реализации токена обновления из предыдущих разделов показывают, что мы можем сохранять сессии и оставаться в системе.

В этом случае простая реализация принудительного выхода просит сервер аутентификации сделать недействительными все токены обновления, ассоциированные с конкретным пользователем.

По сути, это реализация для сервера аутентификации в бэкенде, которая не требует никакой особой обработки на клиенте, за исключением кнопки “Принудительный выход” в приложении.

Фоновое обновление

Существует две главных проблемы, с которыми могут столкнуться пользователи нашего JWT-приложения:

  1. Учитывая короткий срок действия JWT, пользователь будет повторно авторизовываться каждые 15 минут, что было бы ужасно неудобно. Ему наверняка было бы комфортнее оставаться в системе длительное время.
  2. Если пользователь закрывает своё приложение и вновь его открывает, то ему придётся авторизовываться повторно. Его сессия не постоянна, т.к. мы не сохраняем токен на клиенте.

Для решения этих проблем большинство JWT-провайдеров предоставляют токен обновления, который имеет два свойства:

  1. Он может использоваться для совершения вызова API (например, /refresh_token) с запросом нового JWT-токена до истечения срока действия текущего.
  2. Он может быть безопасно сохранён на клиенте в течение нескольких сессий.

Как работает обновляемый токен?

Этот токен выдаётся в процессе аутентификации вместе с JWT. Сервер аутентификации сохраняет его и ассоциирует с конкретным пользователем в своей базе данных, чтобы иметь возможность обрабатывать логику обновления JWT.

На клиенте до момента истечения текущего токена мы подключаем наше приложение, чтобы сделать конечную точку refresh_oken и получить новый JWT.

Как осуществляется безопасное хранение токена обновления на клиенте?

Похожее:  Как реализовать интеграцию с ЕСИА на Java без лишних проблем / Хабр

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *