Token based authentication for django rest framework

Setting up the Scenario

Inline with the OAuth2 specification, apart from our Client – which is our focus subject in this article – we naturally need an Authorization Server and a Resource Server. 

We can use well-known authorization providers like Google or Github. To better understand the role of the OAuth2 Client, we can also use our own servers, with an implementation available in here. We won’t show the full configuration since it’s not the topic of this tutorial, it’s enough knowing that:

  • the Authorization Server will be:
    • running on port 8081
    • exposing the /oauth/authorize, /oauth/token and oauth/check_token endpoints to carry out the desired functionality
    • configured with sample users (e.g. john/123) and a single OAuth client (fooClientIdPassword/secret)
  • the Resource Server will be separated from the Authentication Server and will be:
    • running on port 8082
    • serving a simple Foo object secured resource accessible using the /foos/{id} endpoint

Note: it’s important to understand that several Spring projects are offering different OAuth-related features and implementations. We can examine what each library provides in this Spring Projects matrix.

The WebClient and all the reactive Webflux related functionality is part of the Spring Security 5 project. Therefore, we’ll mainly be using this framework throughout this article.

Логин, создание сессии/токенов (api/auth/login):

  1. Пользователь логинится в приложении, передавая логин/пароль и fingerprint браузера (ну или некий иной уникальный индентификатор устройства если это не браузер)
  2. Сервер проверят подлинность логина/пароля,
  3. В случае удачи создает и записывает сессию в БД
  4. Отправляет клиенту два токена access и refresh token uuid (взятый из выше созданной сессии)
  1. Клиент сохраняет токены(access в памяти приложения, refresh персистентно), используя access token для последующей авторизации запросов.

Перед каждым запросом клиент предварительно проверяет время жизни access token’а (да берем прямо из JWT в клиентском приложении) и если оно истекло использует refresh token чтобы обновить ОБА токена и продолжает использовать новый access token. Для большей уверенности можем обновлять токены на несколько секунд раньше.

Что такое fingerprint ? Это инструмент отслеживания браузера вне зависимости от желания пользователя быть идентифицированным. Это хеш сгенерированный js’ом на базе неких уникальных параметров/компонентов браузера. Преимущество fingerprint’a в том что он нигде персистентно не хранится и генерируется только в момент логина и рефреша.

  • Библиотека для хеширования: https://github.com/Valve/fingerprintjs2
  • Пример ф-ции получения такого хеша: https://gist.github.com/zmts/b26ba9a61aa0b93126fc6979e7338ca3

Login Using Google

Now, we’ll configure our application to support login using Google.

First, we need to create a new project at Google Developer Console

Now, we need to add OAuth2 credentials (Create Credentials > OAuth Client ID).

Next, we’ll add this to “Authorized Redirect URIs”:

http://localhost:8080/login/oauth2/code/google

Then, we need to configure our application.yml to use the Client ID and Secret:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: YOUR_APP_CLIENT_ID
            client-secret: YOUR_APP_CLIENT_SECRET

As we have spring-security-oauth2-client in our path, our application will be secured.

Users will be redirected to log in using Google before they can access our home page.

Configuring the Key Used for JWT Validation

Now that the configuration is in place, all requests routed to the /oauth/** path will be allowed through anonymously, while all other requests will require authentication.

There is one thing we’re missing here though, and that’s the actual secret required to verify that the JWT is valid. To do that, we need to provide the key (which is symmetric in this case) used to sign the JWT. Rather than writing the configuration code manually, we can use spring-security-oauth2-autoconfigure.

Let’s start by adding the artifact to our project:

org.springframework.security.oauth.bootspring-security-oauth2-autoconfigure2.1.2.RELEASE

Next, we need to add a few lines of configuration to our application.yaml file to define the key used to sign the JWT:

security:
  oauth2:
    resource:
      jwt:
        key-value: 123

The line key-value: 123 sets the symmetric key used by the Authorization Server to sign the JWT. This key will be used by spring-security-oauth2-autoconfigure to configure token parsing.

It’s important to note that, in a production system, we shouldn’t use a symmetric key, specified in the source code of the application. That naturally needs to be configured externally.

Security Across Layers

It’s important to note that the JWT is being validated by the Zuul edge service before being passed into the Resource Server. If the JWT is invalid, then the request will be denied at the edge service boundary.

If the JWT is indeed valid on the other hand, the request is passed on downstream. The Resource Server then validates the JWT again and extracts key fields such as user scope, organization (in this case a custom field) and authorities. It uses these fields to decide what the user can and can’t do.

To be clear, in a lot of architectures, we won’t actually need to validate the JWT twice – that’s a decision you’ll have to make based on your traffic patterns.

For example, in some production projects, individual Resource Servers may be accessed directly, as well as through the proxy – and we may want to verify the token in both places. In other projects, traffic may be coming only through the proxy, in which case verifying the token there is enough.

Тип разрешения на авторизацию: Код авторизации

Код авторизации является одним из наиболее распространённых типов разрешения на авторизацию, поскольку он хорошо подходит для серверных приложений, где исходный код приложения и секрет клиента не доступны посторонним. Процесс в данном случае строится на перенаправлении (redirection), что означает, что приложение должно быть в состоянии взаимодействовать с пользовательским агентом (user-agent), например, веб-браузером, и получать коды авторизации API, перенаправляемые через пользовательский агент.

Опишем процесс на диаграмме:

Шаг 1: Ссылка с кодом авторизации

Сначала пользователю предоставляется ссылка следующего вида:

Рассмотрим компоненты ссылки:

  • https://cloud.digitalocean.com/v1/oauth/authorize: входная точка API авторизации (API authorization endpoint).
  • client_id=CLIENT_ID: идентификатор клиента приложения (с помощью этого идентификатора API понимает, какое приложение запрашивает доступ).
  • redirect_uri=CALLBACK_URL: URL, на который сервис перенаправит пользовательского агент (браузер) после выдачи авторизационного кода.
  • response_type=code: указывает на то, что приложение запрашивает доступ с помощью кода авторизации.
  • scope=read: задаёт уровень доступа приложения (в данном случае — доступ на чтение).

Шаг 2: Пользователь авторизует приложение

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

На этом скриншоте экрана авторизации DigitalOcean мы можем видеть, что приложение “Thedropletbook App” запрашивает доступ на чтение к аккаунту “manicas@digitalocean.com”.

Шаг 3: Приложение получает код авторизации

Если пользователь выбирает “Авторизовать приложение”, сервис перенаправляет пользовательский агент (браузер) по URL перенаправления (redirect URL), который был задан на этапе регистрации клиента (вместе с кодом авторизации). Ссылка будет выглядеть похожим образом (в данном примере приложение называется “dropletbook.com”):

Шаг 4: Приложение запрашивает токен доступа

Приложение запрашивает токен доступа у API путём отправки авторизационного кода и аутентификационной информации (включая секрет клиента) сервису. Ниже представлен пример POST-запроса для получения токена DigitalOcean:

Шаг 5: Приложение получает токен доступа

Если авторизация прошла успешно, API возвращает токен доступа (а также, опционально, токен для обновления токена доступа — refresh token). Весь ответ сервера может выглядеть следующим образом:

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

Introduction

Simply put, a microservice architecture allows us to break up our system and our API into a set of self-contained services, which can be deployed fully independently.

While this is great from a continuous deployment and management point of view, it can quickly become convoluted when it comes to API usability. With different endpoints to manage, dependent applications will need to manage CORS (Cross-Origin Resource Sharing) and a diverse set of endpoints.

Zuul is an edge service that allows us to route incoming HTTP requests into multiple backend microservices. For one thing, this is important for providing a unified API for consumers of our backend resources.

Basically, Zuul allows us to unify all of our services by sitting in front of them and acting as a proxy. It receives all requests and routes them to the correct service. To an external application, our API appears as a unified API surface area.

In this tutorial, we’ll talk about how we can use it for this exact purpose, in conjunction with an OAuth 2.0 and JWTs, to be the front line for securing our web services. Specifically, we’ll be using the Password Grant flow to obtain an Access Token to the protected resources.

A quick but important note is that we’re only using the Password Grant flow to explore a simple scenario; most clients will more likely be using the Authorization Grant flow in production scenarios.

An Alternative – Client Registration in the Call

Earlier, we saw that using the setDefaultOAuth2AuthorizedClient implies that the application will include the access token in any call we realize with the client.

If we remove this command from the configuration, we’ll need to specify the client registration explicitly by the time we define the request.

One way, of course, is by using the clientRegistrationId as we did before when working in the client credentials flow.

Since we associated the Principal with authorized clients, we can obtain the OAuth2AuthorizedClient instance using the @RegisteredOAuth2AuthorizedClient annotation:

@GetMapping("/auth-code-annotated")
Mono useOauthWithAuthCodeAndAnnotation(
  @RegisteredOAuth2AuthorizedClient("bael") OAuth2AuthorizedClient authorizedClient) {
    Mono retrievedResource = webClient.get()
      .uri("http://localhost:8084/retrieve-resource")
      .attributes(
        ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient))
      .retrieve()
      .bodyToMono(String.class);
    return retrievedResource.map(string -> 
      "Resource: " + string 
        + " - Principal associated: " + authorizedClient.getPrincipalName() 
        + " - Token will expire at: " + authorizedClient.getAccessToken()
          .getExpiresAt());
}

Remove the Access Token from the AngularJS Client

Besides revoking the access token from the token store, the access_token cookie will also need to be removed from the client side.

Let’s add a method to our AngularJS controller that clears the access_token cookie and calls the /oauth/token/revoke DELETE mapping:

$scope.logout = function() {
    logout($scope.loginData);
}
function logout(params) {
    var req = {
        method: 'DELETE',
        url: "oauth/token"
    }
    $http(req).then(
        function(data){
            $cookies.remove("access_token");
            window.location.href="login";
        },function(){
            console.log("error");
        }
    );
}

This function will be called when clicking on the Logout link:

Получение маркера доступаGet an Access Token

Кнопка войти отправляет запрос в конечную точку маркера.The Log In button sends a request to the token endpoint. Текст запроса содержит следующие данные в формате URL-адреса:The body of the request contains the following form-url-encoded data:

  • Тип_предоставления: «пароль»grant_type: «password»
  • имя пользователя. username:
  • пароль: password:

Ниже приведен код JavaScript, который отправляет запрос AJAX:Here is the JavaScript code that sends the AJAX request:

Если запрос выполнен, сервер авторизации возвращает маркер доступа в тексте ответа.If the request succeeds, the authorization server returns an access token in the response body

Обратите внимание, что мы сохраняем токен в хранилище сеанса, чтобы использовать его позже при отправке запросов в API.Notice that we store the token in session storage, to use later when sending requests to the API. В отличие от некоторых форм проверки подлинности (например, проверки подлинности на основе файлов cookie), браузер не будет автоматически включать маркер доступа в последующие запросы.Unlike some forms of authentication (such as cookie-based authentication), the browser will not automatically include the access token in subsequent requests

Приложение должно сделать это явным образом.The application must do so explicitly. Это хорошая вещь, так как она ограничивает CSRF уязвимости.That’s a good thing, because it limits CSRF vulnerabilities.

HTTP-запрос:HTTP request:

Как видите, запрос содержит учетные данные пользователя.You can see that the request contains the user’s credentials. Для обеспечения безопасности транспортного уровня необходимо использовать протокол HTTPS.You must use HTTPS to provide transport layer security.

HTTP-ответ:HTTP response:

Для удобочитаемости я создам отступ для JSON и усекает маркер доступа, который довольно длинный.For readability, I indented the JSON and truncated the access token, which is a quite long.

Свойства , и определяются спецификацией OAuth2. Другие свойства (, и ) предназначены только для информационных целей.The , , and properties are defined by the OAuth2 spec. The other properties (, , and ) are just for informational purposes. Код, который добавляет эти дополнительные свойства в метод , можно найти в файле/Провидерс/аппликатионоауспровидер.КС.You can find the code that adds those additional properties in the method, in the /Providers/ApplicationOAuthProvider.cs file.

WebClient

We can also do more than just authenticate users using OAuth2. We can use WebClient to access OAuth2 secured resources using OAuth2AuthorizedClient.

Now, let’s configure our WebClient:

@Bean
public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrationRepo, 
  ServerOAuth2AuthorizedClientRepository authorizedClientRepo) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction filter = 
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo, authorizedClientRepo);
    
    return WebClient.builder().filter(filter).build();
}

Then, we can retrieve an OAuth2 secured resource:

@Autowired
private WebClient webClient;

@GetMapping("/foos/{id}")
public Mono getFooResource(@RegisteredOAuth2AuthorizedClient("custom") 
  OAuth2AuthorizedClient client, @PathVariable final long id){
    return webClient
      .get()
      .uri("http://localhost:8088/spring-security-oauth-resource/foos/{id}", id)
      .attributes(oauth2AuthorizedClient(client))
      .retrieve()
      .bodyToMono(Foo.class); 
}

Note that we retrieved the remote resource Foo using AccessToken from OAuth2AuthorizedClient.

Spring Security 5 Support – The Client Credentials Flow

Spring Security allows configuring our application as an OAuth2 Client.

In this write-up, we’ll use a WebClient instance to retrieve resources using the ‘Client Credentials’ grant type first, and then using the ‘Authorization Code’ flow.

The first thing we’ll have to do is configure the client registration and the provider that we’ll use to obtain the access token.

4.1. Client and Provider Configurations

As we’ve seen in , we can either configure it programmatically or rely on the Spring Boot auto-configuration by using properties to define our registration:

spring.security.oauth2.client.registration.bael.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret

spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token

These are all the configurations that we need to retrieve the resource using the client_credentials flow.

4.2. Using the WebClient

We use this grant type in machine-to-machine communications where there’s no end-user interacting with our application.

For example, let’s imagine we have a cron job trying to obtain a secured resource using a WebClient in our application:

@Autowired
private WebClient webClient;

@Scheduled(fixedRate = 5000)
public void logResourceServiceResponse() {

    webClient.get()
      .uri("http://localhost:8084/retrieve-resource")
      .retrieve()
      .bodyToMono(String.class)
      .map(string 
        -> "Retrieved using Client Credentials Grant Type: " + string)
      .subscribe(logger::info);
}

4.3. Configuring the WebClient

Next, let’s set the webClient instance that we’ve autowired in our scheduled task:

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
      new ServerOAuth2AuthorizedClientExchangeFilterFunction(
        clientRegistrations,
        new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    oauth.setDefaultClientRegistrationId("bael");
    return WebClient.builder()
      .filter(oauth)
      .build();
}

As we said, the client registration repository is automatically created and added to the context by Spring Boot.

The next thing to notice here is that we’re using a UnAuthenticatedServerOAuth2AuthorizedClientRepository instance. This is due to the fact that no end-user will take part in the process since it’s a machine-to-machine communication. Finally, we stated that we’d use the bael client registration by default.

Otherwise, we’d have to specify it by the time we define the request in the cron job:

webClient.get()
  .uri("http://localhost:8084/retrieve-resource")
  .attributes(
    ServerOAuth2AuthorizedClientExchangeFilterFunction
      .clientRegistrationId("bael"))
  .retrieve()
  // ...

4.4. Testing

If we run our application with the DEBUG logging level enabled, we’ll be able to see the calls that Spring Security is doing for us:

o.s.w.r.f.client.ExchangeFunctions:
  HTTP POST http://localhost:8085/oauth/token
o.s.http.codec.json.Jackson2JsonDecoder:
  Decoded 
o.s.w.r.f.client.ExchangeFunctions:
  HTTP GET http://localhost:8084/retrieve-resource
o.s.core.codec.StringDecoder:
  Decoded "This is the resource!"
c.b.w.c.service.WebClientChonJob:
  We retrieved the following resource using Client Credentials Grant Type: This is the resource!

We’ll also notice that the second time the task runs, the application requests the resource without asking for a token first since the last one hasn’t expired.

Рефреш токенов (api/auth/refresh-tokens):

Для использования возможности аутентификации на более чем одном девайсе необходимо хранить все рефреш токены по каждому юзеру. Я храню это список в PostgreSQL таблице. В процессе каждого логина создается запись с IP/Fingerprint и другой мета информацией то есть сессия.

  1. Клиент(фронтенд) проверяет перед запросом не истекло ли время жизни access token’на
  2. Если истекло клиент отправляет на
  3. Сервер получает запись сессии по UUID’у рефреш токена
  4. Cохраняет текущую сессию в переменную и удаляет ее из таблицы
  5. Проверяет текущую сессию:
    1. Не истекло ли время жизни
    2. На соответствие старого fingerprint’a полученного из текущей сессии с новым полученным из тела запроса
  6. В случае негативного результата бросает ошибку /
  7. В случае успеха создает новую сессию и записывает ее в БД
  8. Создает новый access token
  9. Отправляет клиенту

Стоит заметить что процесс добавления сессии в таблицу должен имеет свои меры безопасности. При добавлении стоит проверять сколько сессий всего есть у юзера и если их слишком много или юзер конектится одновременно из нескольких подсетей, стоит предпринять меры. Имплементируя данную проверку я проверяю только что бы юзер имел максимум до 5 одновременных сессий максимум, и на 6’ой удаляю все остальные сессии кроме текущей(6’ой). Все остальные проверки на ваше усмотрение в зависимости от задачи.

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

Related

  • Tutorial

    Автор выбрал организацию Open Sourcing Mental Illness Ltd для получения пожертвований в рамках программы Write for DOnations. Kubernetes, платформа для управления контейнерами с…

  • Tutorial

    Часто бывает трудно управлять несколькими машинами на ежедневной основе. Хотя Security Shell (SSH) — это хороший инструмент для удаленного доступа, сам протокол имеет определенные недостатки в отношении удобства и безопасности. Например, удаленные компьютеры должны иметь…

  • Tutorial

    Автор выбрал фонд Free and Open Source Fund для получения пожертвования в рамках программы Write for DOnations. Хотя используемая по умолчанию установка сервера [Apache…

  • Tutorial
    Настройка ключей SSH в CentOS 8

    SSH или защищенная оболочка — это шифрованный протокол, используемый для администрирования и связи с серверами. При работе с сервером CentOS вы с большой вероятностью проведете больше всего времени в сеансах терминала с подключением к серверу через SSH. В этом обучающем…

Store Google ClientID и ClientSecretStore Google ClientID and ClientSecret

Храните конфиденциальные параметры, такие как Google и с диспетчером секретов.Store sensitive settings such as the Google and with the Secret Manager. В рамках этого руководства назовите маркеры и :For the purposes of this tutorial, name the tokens and :

При работе с иерархическими ключами в переменных среды разделитель-двоеточие () может работать не на всех платформах (например, в Bash).When working with hierarchical keys in environment variables, a colon separator () may not work on all platforms (for example, Bash). Двойной знак подчеркивания () поддерживается на всех платформах и автоматически заменяется двоеточием.A double underscore () is supported by all platforms and is automatically replaced by a colon.

Вы можете управлять учетными данными и использованием API в консоли API.You can manage your API credentials and usage in the API Console.

Переадресация сведений запроса с помощью прокси-сервера или подсистемы балансировки нагрузкиForward request information with a proxy or load balancer

Если приложение развертывается с прокси-сервером или подсистемой балансировки нагрузки, некоторые сведения из исходного запроса можно перенаправить в приложение в заголовках запроса.If the app is deployed behind a proxy server or load balancer, some of the original request information might be forwarded to the app in request headers. Эти сведения обычно включают безопасную схему запроса (), узел и IP-адрес клиента.This information usually includes the secure request scheme (), host, and client IP address. Приложения не считывают автоматически и не используют эти заголовки запроса.Apps don’t automatically read these request headers to discover and use the original request information.

Схема используется для создания ссылок, определяющих процесс проверки подлинности с помощью внешних поставщиков.The scheme is used in link generation that affects the authentication flow with external providers. Потеря безопасной схемы () приводит к тому, что приложение неправильно выполняет перенаправление на небезопасные URL-адреса.Losing the secure scheme () results in the app generating incorrect insecure redirect URLs.

Используйте ПО промежуточного слоя для перенаправленных заголовков, чтобы предоставить приложению сведения из исходных запросов для обработки запросов.Use Forwarded Headers Middleware to make the original request information available to the app for request processing.

Для получения дополнительной информации см. Настройка ASP.NET Core для работы с прокси-серверами и подсистемами балансировки нагрузки.For more information, see Настройка ASP.NET Core для работы с прокси-серверами и подсистемами балансировки нагрузки.

Ссылка на основную публикацию