Платформа 3V/Инструкция по работе с платформой/Принципы работы платформы 3V с аутентификацией и авторизацией: различия между версиями
м (A.saydakova переименовал страницу Платформа 3V/Принципы работы платформы 3V с аутентификацией и авторизацией в Платформа 3V/Инструкция по работе с платформой/Принципы работы платформы 3V с аутентификацией и авторизацией) |
|
(нет различий)
|
Текущая версия на 15:28, 10 августа 2021
Содержание
- 1 Введение
- 2 Принципы авторизации
- 3 Пример минимального приложения, реализующего oidc сервер авторизации
- 4 Настройки файлов конфигурации сервисов бэкенда
- 5 Пример подключения сервера авторизации, работающего по протоколу oidc или по протоколу oauth 2.0 с поддержкой метаданных
- 6 Пример подключения сервера авторизации oauth 2.0 без поддержки метаданных, но реализующего стандартные эндпоинты, в том числе jwks_uri
- 7 Пример подключения сервера авторизации oauth 2.0 без поддержки метаданных, но реализующего стандартные эндпоинты, кроме jwks_uri
Введение
Для аутентификации и авторизации в платформе используется протокол 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, в частности:
- Keycloak (https://www.keycloak.org/)
- IdentityServer4 (https://github.com/IdentityServer/IdentityServer4)
- Auth0 (https://auth0.com/)
- Active Directory Federation Services (https://docs.microsoft.com/ru-ru/windows-server/identity/active-directory-federation-services)
И так далее.
Стоит упомянуть также, что именно аутентификация, по-большому счету, может происходить совершенно произвольным способом, особенно если вместо стандартного веб-интерфейса платформы используется какой-то пользовательский. А вот уже авторизация на стороне бэкенда работает с access-токенами, которые больше относятся именно к OAuth 2.0. По-большому счету, весь процесс авторизации проходит в рамках чистого OAuth 2.0, из OIDC там лишь опционально используется механизм получения метаданных сервера авторизации (который, впрочем, по стандарту тоже относится к OAuth 2.0 - https://datatracker.ietf.org/doc/html/rfc8414).
Соответственно, для того, чтобы подключить любую стороннюю авторизацию к платформе, достаточно на стороне веба получать любой JWT access-токен, который сможет отвалидироваться бэкендом. Применительно к произвольному конкретному серверу авторизации есть два варианта:
- Этот сервер умеет работать по протоколу OIDC в его общепринятой стандартной реализации на базе JWT. Тогда достаточно использовать его соответствующее API, задать нужные разделы в файлах конфигурации - и все заработает.
- Этот сервер не умеет работать по протоколу OIDC в его общепринятой стандартной реализации. Тогда между платформой и этим сервером можно реализовать и поставить некоторый транслирующий сервер авторизации, который OIDC-совместимые запросы будет преобразовывать в те запросы, которые понимает имеющийся сервер, а ответы имеющегося сервера преобразовывать в OIDC-совместимые запросы.
Принципы авторизации
Авторизация в платформе происходит на основании ролей, указанных в пришедшем access-токене, а также на основании заданных уже в самой платформе доступов к операциям и/или объектам для соответствующих ролей. По сути просто происходит сопоставление тех ролей, которые указаны в токене, с теми, которые заданы в платформе. Если эти два множества имеют непустое пересечение - выдается максимальный доступ из пересечения. Если имеют пустое пересечение - доступ запрещается с кодом 403.
Пример минимального приложения, реализующего oidc сервер авторизации
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 }