Платформа 3V/Инструкция по работе с платформой/Принципы работы платформы 3V с аутентификацией и авторизацией

Материал из 3v-wiki
Перейти к навигации Перейти к поиску

Введение

Для аутентификации и авторизации в платформе используется протокол OIDC (https://openid.net/connect/), реализованный общепринятым стандартным способом на основе JWT (https://datatracker.ietf.org/doc/html/rfc7519); кроме этого, есть возможность использовать протокол OAuth 2.0 (https://datatracker.ietf.org/doc/html/rfc6749), также реализованный общепринятым стандартным способом на основе JWT. Почему важно упоминание об общепринятой реализации? Потому что, если вдумчиво вчитаться в вышеуказанные стандарты, то достаточно быстро становится ясно, что ощутимая их часть (особенно OAuth 2.0) описана как implementation specific и/или out of scope. Что это означает? Что протоколы (опять же, особенно OAuth 2.0) не вдаются в подробности деталей своей технической реализации, а больше описывают сам принцип своей работы. Если взять OAuth 2.0, то там неопределена бОльшая часть деталей; если взять OIDC - в нем достаточно детально описан путь аутентификации, но путь авторизации, опять же, отдается на откуп реализации (что неудивительно, поскольку сам по себе OIDC построен поверх OAuth 2.0).

К чему это потенциально приводит и реально приводило на практике? К тому, что каждый, кто делает поддержку этих протоколов (особенно это проявлялось в самом начале их, протоколов, становления, и, особенно, опять же, OAuth 2.0), приходил в итоге к какой-то своей частной/специфичной реализации, которая работала только у него. Соответственно, OAuth 2.0 сервер аутентификации Google, к примеру, мог работать только по спецификации того же Google с сервисами Google - ну или вам приходилось тоже поддерживать эту спецификацию (например, у Google токены изначально имели произвольный формат, не JWT; с одной стороны, конечно, можно предположить здесь использование reference/opaque-токенов, но с другой, стандарт на это (https://datatracker.ietf.org/doc/html/rfc7662), опять же, утвердился несколькими годами позже, и вполне вероятно, что как реакция на текущее положение дел).

Видя все это, комитет по стандартизации с течением времени разразился аж порядка 10 стандартов-дополнений к изначальному OAuth 2.0 (https://datatracker.ietf.org/doc/html/rfc8252, https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics, https://datatracker.ietf.org/doc/html/rfc6750 и т.д.), и в конечном счете все эти дополнения собрал в OAuth 2.1 (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-02), впрочем, опять не фиксируя явно специфику реализации (того же формата токенов и/или конкретных используемых механизмов; спасибо и на том, что конкретика приводится хотя бы в качестве примеров со ссылками на соответствующие другие стандарты). Видя все это, отрасль в целом постепенно пришла к некоторому единому пониманию использования и реализации этих протоколов, что вылилось в стандартные библиотеки для всех языков, которые можно подключить и сразу использовать, и в стандартные же реализации на стороне серверов авторизации.

В частности, на стороне веба платформа использует библиотеку angular-oauth2-oidc (https://github.com/manfredsteyer/angular-oauth2-oidc, https://www.npmjs.com/package/angular-oauth2-oidc) для самого процесса аутентификации, а для валидации и процессинга access-токена на стороне бэкэнда - стандартную реализацию .NET из пакета Microsoft.AspNetCore.Authentication.JwtBearer (https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer). Вкупе это все дает возможность использовать любой современный сервер авторизации, работающий по протоколу oidc/oauth2.0, в частности:

И так далее.

Стоит упомянуть также, что именно аутентификация, по-большому счету, может происходить совершенно произвольным способом, особенно если вместо стандартного веб-интерфейса платформы используется какой-то пользовательский. А вот уже авторизация на стороне бэкенда работает с access-токенами, которые больше относятся именно к OAuth 2.0. По-большому счету, весь процесс авторизации проходит в рамках чистого OAuth 2.0, из OIDC там лишь опционально используется механизм получения метаданных сервера авторизации (который, впрочем, по стандарту тоже относится к OAuth 2.0 - https://datatracker.ietf.org/doc/html/rfc8414).

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

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

Принципы авторизации

Авторизация в платформе происходит на основании ролей, указанных в пришедшем access-токене, а также на основании заданных уже в самой платформе доступов к операциям и/или объектам для соответствующих ролей. По сути просто происходит сопоставление тех ролей, которые указаны в токене, с теми, которые заданы в платформе. Если эти два множества имеют непустое пересечение - выдается максимальный доступ из пересечения. Если имеют пустое пересечение - доступ запрещается с кодом 403.

Пример минимального приложения, реализующего oidc сервер авторизации

AuthController.cs
  1 using System;
  2 using System.Collections.Generic;
  3 using System.IdentityModel.Tokens.Jwt;
  4 using System.Linq;
  5 using System.Threading.Tasks;
  6 using Microsoft.AspNetCore.Authorization;
  7 using Microsoft.AspNetCore.Mvc;
  8 using Microsoft.Extensions.Localization;
  9 using Microsoft.IdentityModel.Protocols.OpenIdConnect;
 10 using Microsoft.IdentityModel.Tokens;
 11 using Newtonsoft.Json;
 12 using Newtonsoft.Json.Linq;
 13 using Trivium.Common.Namespaces;
 14 using Trivium.CommonWeb.Authorization;
 15 using Trivium.CommonWeb.Routes;
 16 using Trivium.Users.Core.Interfaces;
 17 using Trivium.Users.Core.Resources;
 18 using Trivium.Users.WebApi.Extensions;
 19 
 20 namespace Trivium.Users.WebApi.Controllers
 21 {
 22     /// <summary>
 23     /// Контроллер имитации работы с OIDC.
 24     /// </summary>
 25     public class AuthController : RoutedControllerBase
 26     {
 27         private readonly OpenIdConnectConfiguration _settings;
 28         private readonly JsonWebKeySet _certs;
 29         private readonly SigningCredentials _creds;
 30         private readonly IUserProvider _userProvider;
 31         private readonly string _issuer;
 32         private const int TokenLifeTime = 1440;
 33         private const int RefreshTokenLifeTime = 1800;
 34         private readonly IStringLocalizer<UsersResources> _localizer;
 35 
 36         public AuthController(IUserProvider userProvider, 
 37                               IStringLocalizer<UsersResources> localizer,
 38                               ICurrentNamespaceAccessor currentNamespaceAccessor)
 39         {
 40             _userProvider = userProvider;
 41             _localizer = localizer;
 42             
 43             // предполагаем, что здесь всегда дефолт. если нет - ну поправить как надо
 44             var authConfig = currentNamespaceAccessor.Namespace.Authorizations.GetDefault();
 45 
 46             _settings = authConfig.OidcConfiguration ?? throw new ArgumentException("Не заданы настройки OIDC");
 47             _certs = _settings.JsonWebKeySet;
 48             _creds = authConfig.GetSigningCredentials();
 49 
 50             _issuer = authConfig.Authority;
 51         }
 52 
 53         /// <summary>
 54         /// Авторизация и продление токена.
 55         /// </summary>
 56         [AllowAnonymous]
 57         [Route("realms/{realm}/protocol/openid-connect/token")]
 58         [HttpPost]
 59         public async Task<JObject> TokenAsync(string realm)
 60         {
 61             var values = ContextValues();
 62             if (!values.TryGetValue("grant_type", out var grantType))
 63                 throw new ArgumentException(_localizer["GrantTypeNotSet"]);
 64 
 65             var now = DateTime.UtcNow;
 66             switch (grantType)
 67             {
 68                 case "password":
 69                 {
 70                     var userName = values["username"];
 71                     var userInfo = await _userProvider.GetUserInfoAsync(userName);
 72                     var encodedToken = userInfo.GetUserToken(_issuer, _creds, now);
 73                     return ResultTokenResponse(encodedToken, now);
 74                 }
 75                 case "refresh_token":
 76                 {
 77                     var tokenString = values["refresh_token"];
 78                     var handler = new JwtSecurityTokenHandler();
 79                     var jsonToken = (JwtSecurityToken)handler.ReadToken(tokenString);
 80                     var userName = jsonToken.Claims.First(claim => claim.Type == "name").Value;
 81                     var userInfo = await _userProvider.GetUserInfoAsync(userName);
 82                     var encodedToken = userInfo.GetUserToken(_issuer, _creds, now);
 83                         return ResultTokenResponse(encodedToken, now);
 84                 }
 85                 default:
 86                     throw new ArgumentException(_localizer["UnknowGrantTypeValue", grantType]);
 87             }
 88         }
 89 
 90         /// <summary>
 91         /// Конфигурация oidc.
 92         /// </summary>
 93         [AllowAnonymous]
 94         [Route("realms/{realm}/.well-known/openid-configuration")]
 95         [HttpGet]
 96         public JObject OpenIdConfiguration(string realm)
 97         {
 98             return JsonConvert.DeserializeObject<JObject>(OpenIdConnectConfiguration.Write(_settings));
 99         }
100 
101         /// <summary>
102         /// Возвращает ключи шифрования для oidc
103         /// </summary>
104         [AllowAnonymous]
105         [Route("realms/{realm}/protocol/openid-connect/certs")]
106         [HttpGet]
107         public JObject OpenIdCerts(string realm)
108         {
109             return JsonConvert.DeserializeObject<JObject>(JsonConvert.SerializeObject(_certs));
110         }
111 
112         /// <summary>
113         /// Ответ с токеном для запросов через oidc.
114         /// </summary>
115         private static JObject ResultTokenResponse(string token, DateTime dateUtc)
116         {
117             return new JObject
118             {
119                 {"access_token", token},
120                 {"expires_in", TokenLifeTime},
121                 {"id_token", token},
122                 {"not-before-policy", dateUtc.Ticks},
123                 {"refresh_expires_in", RefreshTokenLifeTime},
124                 {"refresh_token", token},
125                 {"scope", "openid email profile"},
126                 {"session_state", Guid.NewGuid()},
127                 {"token_type", "bearer"}
128             };
129         }
130 
131         /// <summary>
132         /// Получение данных из Form-data в формате справочника.
133         /// </summary>
134         private Dictionary<string, string> ContextValues()
135         {
136             var form = HttpContext.Request.Form;
137             return form.Keys.ToDictionary(Convert.ToString, key => Convert.ToString(form[key]));
138         }
139 
140         [HttpGet]
141         public string Get() => "started";
142     }
143 }


Настройки файлов конфигурации сервисов бэкенда

В конфигурационном файле каждого сервиса (appsettings.json; или appsettings.Common.json, если общие настройки для всех сервисов вынесены в отдельный файл) есть раздел "Authorization":

 1 "Authorization": {
 2 
 3   "Enable": false, // признак включения/выключения обязательной аутентификации; при значении true неаутентифицированному пользователю будет отказано в произведении операции с кодом 401; при значении false неаутентифицированный пользователь считается администратором
 4 
 5   "ClientId": "client-3d", // идентификатор клиента в терминах OAuth 2.0; значение этого свойства должно быть в атрибуте "aud" токена, чтобы токен считался валидным
 6 
 7   "OIDCConfigurationFileName": "oidc.example.json", // файл, в котором задана конфигурация сервера авторизации, реализующего oidc/oauth2 протокол. Формат файла приведен ниже
 8 
 9   "OIDCSigningKeysFileName": "keys.example.json", // файл, в котором указаны ключи шифрования для валидации токена в виде JWKS. Формат файла приведен ниже
10 
11   "HS256Key": "BnZPK_poYt6nuwWMPg6DR....", // ключ шифрования для валидации токена по алгоритму HS256, в виде строки
12 
13   "Authority": "https://auth.server.com/auth_root", // URL сервера авторизации, присоединив к которому справа строку "/.well-known/openid-configuration", можно получить метаданные сервера авторизации
14 
15   "ExternalAuthority": "https://auth.server.com/external_auth_root", // то же, что и выше, но доступный во внешний мир, для обеспечения работы сваггера (Authority не всегда доступно наружу)
16 
17   "CustomAuthorizationHeader": "MyAuthorizationHeader", // произвольная строка, указывающая на заголовок, в котором приходит токен доступа. По умолчанию (когда не задано), используется стандартный заголовок Authorization
18 
19   "ValidIssuers": ["http://my.valid.issuer", "https://my.another.valid.issuer"], // набор строк, которые считаются допустимыми в качестве значения атрибута "iss" токена; на случай, если токен выдается не по тому URL, на который ведет Authority (ExternalAuthority при этом работает автоматически, явно его значение дублировать в ValidIssuers не нужно)
20 
21   "FetchUserInfo": true/false, // признак необходимости получения утверждений пользователя (произвольных, в том числе ролей) через userinfo_endpoint; используется в случаях, когда в токене приходит минимально необходимая информация, чтобы размер токена не увеличивался
22 
23   "AdminRole": "admin", // роль администратора
24 
25   "RestrictedRole": "restricted", // ограниченная роль - пользователю с этой ролью недоступны некоторые операции, а также в стандартном веб-приложении не отображаются и/или недоступны некоторые возможности: '''дизаблит:''' кнопку сохранения в Методиках, строку формул над гридом; '''эта роль скрывает''': кнопку открытия конструктора (во всех типах вкладок), правый тулбар (со всякими редакторами джсон и остальным) во всех типах вкладок, шестеренку смены типа карточек в Карточках (старые/новые), шестеренку переключения сокетов в Отчетах/Справочниках/Показателях, в левом меню кнопки типов объектов (то есть нет фильтрации и поиска по типам объектов), в навигаторе выпадающая кнопка оздания разных типов объектов, в навигаторе кнопка копирования объектов, в навигаторе кнопка удаления объектов, в навигаторе кнопка создания обновлений, в навигаторе кнопка связанные объекты, в навигаторе в правом меню: редактор джсон, панель раздачи прав, панель создания обновлений
26 
27   "ReadAllObjectsRole": "repo_read_all_objects", // роль, которая видит все объекты
28 
29   "ChangePermissionsRole": "change_permissions", // роль, которая может раздавать права на объекты и операции в платформе
30 
31   "ManageRightsRole": "manage_rights", // роль, которая может управлять списками ролей, пользователей, а также вхождением пользователей в роли
32 
33 }


Пример подключения сервера авторизации, работающего по протоколу oidc или по протоколу oauth 2.0 с поддержкой метаданных

 1 "Authorization": {
 2 
 3   "Enable": false, 
 4 
 5   "ClientId": "client-3d",
 6 
 7   "Authority": "https://auth.server.com/auth_root",
 8 
 9   ...
10 
11 }


Пример подключения сервера авторизации oauth 2.0 без поддержки метаданных, но реализующего стандартные эндпоинты, в том числе jwks_uri

1 "Authorization": {
2 
3   "Enable": false, 
4 
5   "OIDCConfigurationFileName": "auth.config.json",
6 
7   ...
8 
9 }


auth.config.json:

 1 { // объект, реализующий формат метаданных сервера авторизации (https://datatracker.ietf.org/doc/html/rfc8414). Для работоспособности как минимум должны быть заполнены issuer, authorization_endpoint, token_endpoint
 2 
 3   "issuer": "http://my.auth.server/issuer", // должен совпадать с атрибутом "iss" токена
 4 
 5   "authorization_endpoint": "http://my.auth.server/authorize",
 6 
 7   "token_endpoint": "http://my.auth.server/token",
 8 
 9  "jwks_uri": "http://my.auth.server/certs"
10 
11 }


Пример подключения сервера авторизации oauth 2.0 без поддержки метаданных, но реализующего стандартные эндпоинты, кроме jwks_uri

 1 "Authorization": {
 2 
 3   "Enable": false, 
 4 
 5   "OIDCConfigurationFileName": "auth.config.json",
 6 
 7   "OIDCSigningKeysFileName": "certs.json"
 8 
 9   ...
10 
11 }


auth.config.json:

1 {
2 
3   "issuer": "http://my.auth.server/issuer", 
4 
5   "authorization_endpoint": "http://my.auth.server/authorize",
6 
7   "token_endpoint": "http://my.auth.server/token"
8 
9 }


auth.config.json:

1 {
2 
3   "issuer": "http://my.auth.server/issuer", 
4 
5   "authorization_endpoint": "http://my.auth.server/authorize",
6 
7   "token_endpoint": "http://my.auth.server/token"
8 
9 }


certs.json:

 1 {
 2 
 3    "keys": [ // набор объектов, реализующих формат JWK (https://datatracker.ietf.org/doc/html/rfc7517)
 4 
 5       {
 6 
 7          "kid": "1",
 8 
 9          "kty": "RSA",
10 
11          "alg": "RS256",
12 
13          "use": "sig",
14 
15          "n": "qL72O...",
16 
17          "e": "QWER",
18 
19          "x5c": [
20 
21             "MIICnT..E8pIg=="
22 
23  ],
24 
25          "x5t": "kfT...",
26 
27          "x5t#S256": "PFf..."
28 
29  }
30 
31    ]
32 
33 }


или, в случае наличия строки HS256-ключа

 1 "Authorization": {
 2 
 3   "Enable": false, 
 4 
 5   "OIDCConfigurationFileName": "auth.config.json",
 6 
 7   "HS256Key": "BnZPK_poYt6nuwWMPg6DR...."
 8 
 9   ...
10 
11 }


auth.config.json:

1 {
2 
3   "issuer": "http://my.auth.server/issuer", 
4 
5   "authorization_endpoint": "http://my.auth.server/authorize",
6 
7   "token_endpoint": "http://my.auth.server/token"
8 
9 }