ASP.NET Core 5 — JWT Authentication Tutorial with Example API | by Alpesh Patel | C# Programming | Medium

Аутентификация, основанная на сессии

По умолчанию, Django использует сессии для аутентификации. Прежде чем идти дальше, нужно проговорить, что это значит, почему это важно, что такое аутентификация на основе токенов и что такое JSON Web Token Authentication (JWT для краткости), и что из всего этого мы будем использовать далее в статье.

Знакомство: что такое 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.

Почему json web tokes лучше обычных токенов?

При переходе с обычных токенов на JWT мы получаем несколько преимуществ:

  1. JWT – открытый стандарт. Это означает, что что все реализации JWT должны быть довольно похожими, что является преимуществом при работе с разными языками и технологиями. Обычные токены имеют более свободную форму, что позволяет разработчику решать, как лучше всего реализовывать токены.

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

  3. Библиотеки здесь берут на себя основную тяжелую работу. Развертывание собственной системы аутентификации опасно, поэтому мы оставляем важные вещи проверенным «в боях» библиотекам, которым можем доверять.

Предыстория


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

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

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

Генерацию ключа

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

Для работы на java с jwt есть очень удобная библиотека.

На гитхабе есть все инструкции как работать с jwt, но что бы упростить процесс, приведу пример ниже.

Для генерации ключа создадим обычный maven проект и добавим следующие зависимости

И класс, который будет генерировать secret


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

Создание Spring проекта

Процесс создания описывать не буду, так как на эту тему существует множество статей и туториалов. Да и на официальном сайте Spring’а есть

, где в два клика можно создать минимальный проект.

Оставлю только итоговый pom файл

После создания проекта копируем ранее созданный ключ в

application.properties

app.api.jwtEncodedSecretKey=teTN1EmB5XADI5iV4daGVAQhBlTwLMAE LlXZp1JPI2PoQOpgVksRqe79EGOc5opg AmxOOmyk8q1RbfSWcOyg==

TokenHandler

Нам понадобится сервис для генерации и расшифровки токенов.

В токене будет минимум информации о юзере(только его id) и время истечения токена. Для этого создадим интерфейсы.

Для передачи времени жизни токена.

И для передачи ID. Его будет имплементировать сущность юзера

Так же создадим дефолтную имплементацию для интерфейса

Expiration

. По умолчанию токен будет жить 24 часа.


Добавим пару вспомогательных классов.

GeneratedTokenInfo — для информации о сгенерированном токене.TokenInfo — для информации о пришедшем к нам токене.

Теперь сам

TokenHandler

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

TokenHandler.java

package org.website.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.sql.Date;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Optional;

@Service
@Slf4j
public class TokenHandler {

    @Value("${app.api.jwtEncodedSecretKey}")
    private String jwtEncodedSecretKey;

    private final DefaultExpiration defaultExpiration;

    private SecretKey secretKey;

    @Autowired
    public TokenHandler(final DefaultExpiration defaultExpiration) {
        this.defaultExpiration = defaultExpiration;
    }

    @PostConstruct
    private void postConstruct() {
        byte[] decode = Base64.getDecoder().decode(jwtEncodedSecretKey);
        this.secretKey = new SecretKeySpec(decode, 0, decode.length, "HmacSHA512");
    }

    public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy, Expiration expire) {
        if (null == expire || expire.getAuthTokenExpire().isEmpty())
            expire = this.defaultExpiration;

        try {
            final LocalDateTime expireDateTime = expire.getAuthTokenExpire().get().withNano(0);

            String compact = Jwts.builder()
                    .setId(String.valueOf(createBy.getId()))
                    .setExpiration(Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant()))
                    .signWith(this.secretKey)
                    .compact();

            return Optional.of(new GeneratedTokenInfo(compact, expireDateTime));
        } catch (Exception e) {
            log.error("Error generate new token. CreateByID: {}; Message: {}", createBy.getId(), e.getMessage());
        }
        return Optional.empty();
    }

    public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy) {
        return this.generateToken(createBy, this.defaultExpiration);
    }

    public Optional<TokenInfo> extractTokenInfo(final String token) {
        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder()
                    .setSigningKey(this.secretKey)
                    .build()
                    .parseClaimsJws(token);
            return Optional.ofNullable(claimsJws).map(TokenInfo::fromClaimsJws);
        } catch (Exception e) {
            log.error("Error extract token info. Message: {}", e.getMessage());
        }

        return Optional.empty();
    }

}


Заострять внимание не буду, так как с этим все должно быть понятно.

Аннотация и обработчик

Итак, после всех подготовительных работ, перейдем к самому интересному. Как уже было сказано ранее, нам нужна аннотация, которая будет инжектиться в методы контроллера, где нужен авторизованный пользователь.

Создаем аннотацию со следующим кодом

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

required

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

null

. Но к этому мы будем готовы.

Аннотация создана, но еще нужен handler, который и будет доставать из запроса токен, получать из базы пользователя и пробрасывать его в метод контроллера. Для таких случаев у Spring’а есть интерфейс HandlerMethodArgumentResolver. Его и будем имплементировать.

Обработка AuthenticationException

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

AuthenticationException

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


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

ControllerAdvice

Добавим класс обработки нашего эксепшена.

Теперь, в случае, если возникнет исключение

AuthenticationException

, оно будет перехвачено и пользователю вернется json с ошибкой

AUTHENTICATION_ERROR

Adding a new api key

All you have to do is configure the API key in the value field.
By default, only the authorization header mode is enabled in LexikJWTAuthenticationBundle.
You must set the JWT token as below and click on the “Authorize” button.

Bearer MY_NEW_TOKEN

Adding authentication to an api which uses a path prefix

If your API uses a path prefix, the security configuration would look something like this instead:

Asp.net core 5 — jwt authentication tutorial with example api

In this tutorial we’ll go through a simple example of how to implement custom JWT (JSON Web Token) authentication in an ASP.NET Core 5 API with C#.

JSON Web Token (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.

Please refer to below link for more details about JSON Web Tokens.

Configuring api platform

The “Authorize” button will automatically appear in Swagger UI.

Improving tests suite speed

Since now we have a JWT authentication, functional tests require us to log in each time we want to test an API endpoint. This is where Password Hashers come into play.

Hashers are used for 2 reasons:

Installing lexikjwtauthenticationbundle

We begin by installing the bundle:

Then we need to generate the public and private keys used for signing JWT tokens. If you’re using the API Platform distribution, you may run this from the project’s root directory:

Note that the setfacl command relies on the acl package. This is installed by default when using the API Platform docker distribution but may need be installed in your working environment in order to execute the setfacl command.

This takes care of keypair creation (including using the correct passphrase to encrypt the private key), and setting the correct permissions on the keys allowing the web server to read them.

Jwt аутентификация

Необходимые инструменты:

Visual Studio 2022 – Можно загрузить здесь
.Net 5.0 SDK – Можно загрузить здесь

План работы:

  1. Настройка проекта веб-API .NET 5.0.
  2. Настройка аутентификацию JWT
  3. Генерация токена JWT.
  4. Валидация токена JWT, используя кастомное промежуточное ПО и атрибут авторизации.
  5. Тестирование API с использованием Swagger.

Prerequisites

The following must be installed in your system:

Step 1 — setting up the project

Let’s start by setting up the project. In your terminal window, create a directory for the project:

  1. mkdirjwt-and-passport-auth

And navigate to that new directory:

  1. cdjwt-and-passport-auth

Next, initialize a new package.json:

  1. npm init -y

Install the project dependencies:

  1. npminstall --save bcrypt@4.0.1 body-parser@1.19.0 express@4.17.1 jsonwebtoken@8.5.1 mongoose@5.9.15 passport@0.4.1 passport-jwt@4.0.0 passport-local@1.0.0

Step 2 – create files and directories

In step 1, we initialized npm with the command npm init -y, which automatically created a package.json.

Step 2: install the nuget packages

We will install some of the required Entity Framework Core and JWT packages from the NuGet Package Manager for performing database operations from the code.

Now, install Microsoft.EntityFrameworkCore.Tools, Microsoft.AspNetCore.Identity.EntityFrameworkCore, Microsoft.AspNetCore.Identity and Microsoft.AspNetCore.Authentication.JwtBearer, as same as above steps.

Step 3 — setting up registration and login middleware

Passport is an authentication middleware used to authenticate requests.

Step 4 – create a node.js server and connect your database

Now, let’s create our Node.js server and connect our database by adding the following snippets to your app.js, index.js, database.js.env in that order.

In our database.js.

config/database.js:

In our app.js:

jwt-project/app.js

In our index.js:

jwt-project/index.js

If you notice, our file needs some environment variables. You can create a new .env file if you haven’t and add your variables before starting our application.

In our .env.

To start our server, edit the scripts object in our package.json to look like the one shown below.

The snippet above has been successfully inserted into app.js, index.js, and database.js. First, we built our node.js server in index.js and imported the app.js file with routes configured.

Then, as indicated in database.js, we used mongoose to create a connection to our database.

Execute the command npm run dev.

Step 4: authentication using jwt:

Create one models class and add following models on it.

Now, We create an API controller “AuthenticateController” inside the “Controllers” folder.

Add constructor in AuthenticateController as below:

Step 6 – implement register and login functionality

We’ll be implementing these two routes in our application. We will be using JWT to sign the credentials and bycrypt to encrypt the password before storing them in our database.

From the /register route, we will:

Modify the /register route structure we created earlier to look as shown below.

app.js

// ...
app.post("/register", async (req, res) => {
// Our register logic starts here
try {
// Get user input
const { first_name, last_name, email, password } =req.body;
// Validate user input
if (!(email&&password&&first_name&&last_name)) {
res.status(400).send("All input is required");
    }
// check if user already exist
// Validate if user exist in our database
constoldUser=awaitUser.findOne({ email });
if (oldUser) {
returnres.status(409).send("User Already Exist. Please Login");
    }
//Encrypt user password
encryptedPassword=awaitbcrypt.hash(password, 10);
// Create user in our database
constuser=awaitUser.create({
first_name,
last_name,
email:email.toLowerCase(), // sanitize: convert email to lowercase
password:encryptedPassword,
    });
// Create token
consttoken=jwt.sign(
      { user_id:user._id, email },
process.env.TOKEN_KEY,
      {
expiresIn:"2h",
      }
    );
// save user token
user.token=token;
// return new user
res.status(201).json(user);
  } catch (err) {
console.log(err);
  }
// Our register logic ends here
});
// ...

Note: Update your .env file with a TOKEN_KEY, which can be a random string.

Using Postman to test the endpoint, we’ll get the response shown below after successful registration.

For the /login route, we will:

Modify the /login route structure we created earlier to look like shown below.

Using Postman to test, we’ll get the response shown below after a successful login.

Testing

To test your authentication with ApiTestCase, you can write a method as below:

<?phpnamespaceAppTests;useApiPlatformCoreBridgeSymfonyBundleTestApiTestCase;useAppEntityUser;useHautelookAliceBundlePhpUnitReloadDatabaseTrait;classAuthenticationTestextendsApiTestCase{useReloadDatabaseTrait;publicfunctiontestLogin():void{$client=self::createClient();$container=self::getContainer();$user=newUser();$user->setEmail('[email protected]');$user->setPassword($container->get('security.user_password_hasher')->hashPassword($user,'$3CR3T'));$manager=$container->get('doctrine')->getManager();$manager->persist($user);$manager->flush();$response=$client->request('POST','/authentication_token',['headers'=>['Content-Type'=>'application/json'],'json'=>['email'=>'[email protected]','password'=>'$3CR3T',],]);$json=$response->toArray();$this->assertResponseIsSuccessful();$this->assertArrayHasKey('token',$json);$client->request('GET','/greetings');$this->assertResponseStatusCodeSame(401);$client->request('GET','/greetings',['auth_bearer'=>$json['token']]);$this->assertResponseIsSuccessful();}}

Refer to Testing the API for more information about testing API Platform.

What is authentication and authorization

Authentication and authorization are used in security, particularly when it comes to getting access to a system. Yet, there is a significant distinction between gaining entry into a house (authentication) and what you can do while inside (authorization).

Аутентификация пользователей

В Django существует идея бекендов аутентификации. Не вдаваясь в подробности, бекенд – это, по сути, план принятия решения о том, аутентифицирован ли пользователь. Нам нужно создать собственный бекенд для поддержки JWT, поскольку по умолчанию он не поддерживается ни Django, ни Django REST Framework (DRF).

Создайте и откройте файл apps/authentication/backends.py и добавьте в него следующий код:

Верификация токенов

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

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

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

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

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

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

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

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

Вход пользователей в систему

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

LoginSerializer

Откройте файл apps/authentication/serializers.py и добавьте следующий импорт:

from django.contrib.auth import authenticate

После, наберите следующий код сериализатора в конце файла:

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

При использовании 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.

Генерация jwt токена

Давайте создадим контроллер с именем AuthController внутри папки controllers, и добавим метод Auth, который отвечает для проверку учетных данных входа и создадим токен на основе имени пользователя. Мы отметили этот метод с помощью атрибута AllowAnonymous для обхода аутентификации. Этот метод ожидает объект LoginModel для имени пользователя и пароля.

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

В демонстрационных целях я жестко запрограммировал значения внутри метода для проверки модели.

Настройка jwt-аутентификации

Чтобы настроить JWT (JSON веб-токен), у нас должен быть установлен пакет Nuget внутри проекта, поэтому давайте сначала добавим зависимости проекта.

Пакеты NuGet для установки

Внутри Visual Studio – нажмите на Tools -> Nuget Package Manager -> Manage Nuget packages for solution.

Установить через консоль.

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 5.0.7

Первый шаг – настроить аутентификацию JWT в нашем проекте. Для этого нам нужно зарегистрировать схему JWT в Swagger Service, используя метод Addauthentication.

Давайте определим службу SWARGER и закрепим за ней авторизацию по JWT.

#region Swagger Configuration
            services.AddSwaggerGen(swagger =>
            {
                //This is to generate the Default UI of Swagger Documentation
                swagger.SwaggerDoc("v1", new OpenApiInfo
                {
                    Version = "v1",
                    Title = "JWT Token Authentication API",
                    Description = "ASP.NET Core 5.0 Web API"
                });
                // To Enable authorization using Swagger (JWT)
                swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
                {
                    Name = "Authorization",
                    Type = SecuritySchemeType.ApiKey,
                    Scheme = "Bearer",
                    BearerFormat = "JWT",
                    In = ParameterLocation.Header,
                    Description = "JWT Authorization header using the Bearer scheme. rnrn Enter 'Bearer' [space] and then your token in the text input below.rnrnExample: "Bearer 12345abcdef"",
                });
                swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                          new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference
                                {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "Bearer"
                                }
                            },
                            new string[] {}
                    }
                });
            });
            #endregion

Добавим службу для выполнения аутентификации, а также вызовем AddJWTBearer для конфигурации авторизации.

#region Authentication
            services.AddAuthentication(option =>
            {
                option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            }).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = false,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) //Configuration["JwtToken:SecretKey"]
                };
            });
            #endregion

В приведенном выше примере мы указали параметры, которые необходимо учитывать для проверки токена.

  • Проверка на сервере (Validateissuer = true), который генерирует токен.
  • Проверка получателя токена, авторизован ли он для получения токена (ValidateAudience = True)
  • Проверка, не истек ли токен и валиден ли ключ подписания эмитента (ValidateLifetime = True)
  • Проверка подписи токена (ValidateissuerSigningKey = True)

Мы должны указать значения для Audience, Issuer и Secret key в этом проекте, мы сохранили эти значения внутри файла appsettings.json.

appsettings.json

Настройка клиента

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

Использование JWT на клиенте GrahQL

Настройка проект .net 5.0 web api

Откройте Visual Studio, выберите «Создать новый проект» и нажмите кнопку «Далее».

Добавьте «Имя проекта» и «Имя решения» также выберите путь, чтобы сохранить проект в этом месте, нажмите «Далее».

Теперь выберите целевой фреймворк .NET 5.0, что мы получаем, как только мы устанавливаем SDK, а также получим еще один вариант для настройки поддержки OpenAPI по умолчанию с помощью этого флажка.

Отображение серверной части (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 от начала до конца:

Перегрузка exception_handler и non_field_errors_key

Одна из настроек DRF под названием EXCEPTION_HANDLER возвращает словарь ошибок. Мы хотим, чтобы имена наших ошибок находились под общим единым ключом, потому нам нужно переопределить EXCEPTION_HANDLER. Так же переопределим и NON_FIELD_ERRORS_KEY, как упоминалось ранее.

Начнем с создания project/exceptions.py, и добавления в него следующего кода:

from rest_framework.views import exception_handler


def core_exception_handler(exc, context):
    # Если возникает исключение, которые мы не обрабатываем здесь явно, мы
    # хотим передать его обработчику исключений по-умолчанию, предлагаемому
    # DRF. И все же, если мы обрабатываем такой тип исключения, нам нужен
    # доступ к сгенерированному DRF - получим его заранее здесь.
    response = exception_handler(exc, context)
    handlers = {
        'ValidationError': _handle_generic_error
    }
    # Определить тип текущего исключения. Мы воспользуемся этим сразу далее,
    # чтобы решить, делать ли это самостоятельно или отдать эту работу DRF.
    exception_class = exc.__class__.__name__

    if exception_class in handlers:
        # Если это исключение можно обработать - обработать :) В противном
        # случае, вернуть ответ сгенерированный стандартными средствами заранее
        return handlers[exception_class](exc, context, response)

    return response


def _handle_generic_error(exc, context, response):
    # Это самый простой обработчик исключений, который мы можем создать. Мы
    # берем ответ сгенерированный DRF и заключаем его в ключ 'errors'.
    response.data = {
        'errors': response.data
    }

    return response

Позаботившись об этом, откройте файл project/settings.py и добавьте новый параметр под названием REST_FRAMEWORK в конец файла:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'project.exceptions.core_exception_handler',
    'NON_FIELD_ERRORS_KEY': 'error',
}

Так переопределяются стандартные настройки DFR. Чуть позже, мы добавим еще одну настройку, когда будем писать представления, требующие аутентификации пользователя. Попробуйте отправить еще один некорректный (с неверными почтой и/или паролем) запрос на вход с помощью Postman – сообщение об ошибке должно измениться.

Полное руководство по управлению jwt во фронтенд-клиентах (graphql)

JWT (JSON Web Token, произносится как ‘jot’ [джот]) становится популярным способом управления аутентификацией. Эта статья ставит целью развенчать мифы о стандарте JWT, рассмотреть его плюсы и минусы, а также познакомиться с лучшими методиками реализации JWT на стороне клиента с учётом безопасности.

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

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

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

Приложения обычно спрашивают пользователей, хотят ли те “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();
})

Пример кода

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

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

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

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

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

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

Регистрация новых пользователей

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

RegistrationSerializer

Создайте файл apps/authentication/serializers.py и наберите туда следующий код:

Сообщить drf про наш аутентификационный бекенд

Мы должны явно указать Django REST Framework, какой бекенд аутентификации мы хотим использовать, аналогично тому, как мы сказали Django использовать нашу пользовательскую модель.

Откройте файл project/settings.py и обновите словарь REST_FRAMEWORK следующим новым ключом:

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'apps.authentication.backends.JWTAuthentication',
    ),
}

Тестирование api с помощью swagger (openapi)

Запустите приложение. Это приведет нас к странице Swagger Index со всей настроенной конфигурацией, которую мы сделали в проекте.

Давайте передадим действительные учетные данные Auth API, чтобы получить токен доступа.

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

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

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

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

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

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

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

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

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

Step 3 – install dependencies

We’ll install several dependencies like mongoose, jsonwebtoken, expressdotenvbcryptjs and development dependency like nodemon to restart the server as we make changes automatically.

We will install mongoose because I will be using MongoDB in this tutorial.

Заключение


Данный метод не претендует на единственное правильное решение. Возможно, кому-то больше по душе использование Spring Security. Но, как уже было сказано в самом начале, этот метод проверен, удобен в использовании и очень хорошо работает.

Step 4 — creating the signup endpoint

Express is a web framework that provides routing. In this step, you will create a route for a signup endpoint.

Create a routes directory:

  1. mkdir routes

Create a routes.js file in this new directory:

  1. nano routes/routes.js

Start by requiring express and passport:

const express =require('express');const passport =require('passport');const router = express.Router();

module.exports = router;

Next, add handling of a POST request for signup:

Итоги

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

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

Adding endpoint to swaggerui to retrieve a jwt token

We can add a POST /authentication_token endpoint to SwaggerUI to conveniently retrieve the token when it’s needed.

To do it, we need to create a decorator:

<?phpdeclare(strict_types=1);namespaceAppOpenApi;useApiPlatformCoreOpenApiFactoryOpenApiFactoryInterface;useApiPlatformCoreOpenApiOpenApi;useApiPlatformCoreOpenApiModel;finalclassJwtDecoratorimplementsOpenApiFactoryInterface{publicfunction__construct(privateOpenApiFactoryInterface$decorated){}publicfunction__invoke(array$context=[]):OpenApi{$openApi=($this->decorated)($context);$schemas=$openApi->getComponents()->getSchemas();$schemas['Token']=newArrayObject(['type'=>'object','properties'=>['token'=>['type'=>'string','readOnly'=>true,],],]);$schemas['Credentials']=newArrayObject(['type'=>'object','properties'=>['email'=>['type'=>'string','example'=>'[email protected]',],'password'=>['type'=>'string','example'=>'apassword',],],]);$schemas=$openApi->getComponents()->getSecuritySchemes()??[];$schemas['JWT']=newArrayObject(['type'=>'http','scheme'=>'bearer','bearerFormat'=>'JWT',]);$pathItem=newModelPathItem(ref:'JWT Token',post:newModelOperation(operationId:'postCredentialsItem',tags:['Token'],responses:['200'=>['description'=>'Get JWT token','content'=>['application/json'=>['schema'=>['$ref'=>'#/components/schemas/Token',],],],],],summary:'Get JWT token to login.',requestBody:newModelRequestBody(description:'Generate new JWT Token',content:newArrayObject(['application/json'=>['schema'=>['$ref'=>'#/components/schemas/Credentials',],],]),),security:[],),);$openApi->getPaths()->addPath('/authentication_token',$pathItem);return$openApi;}}

And register this service in config/services.yaml:

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

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