From 0ea8852ed948e203059e54e515069f374d7745f6 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Sat, 16 Aug 2025 14:07:56 +0300 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=A2=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D0=94=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B0=20(AccessToken)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Http/HttpConnectionContext.cs | 51 +-- .../Http/HttpRequestContext.cs | 13 + .../OneScript.StandardLibrary.csproj | 1 + .../Security/Tokens/AccessTokenContext.cs | 383 ++++++++++++++++++ .../Tokens/AccessTokenSignAlgorithmEnum.cs | 57 +++ tests/access-token.os | 210 ++++++++++ tests/http.os | 18 + 7 files changed, 710 insertions(+), 23 deletions(-) create mode 100644 src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs create mode 100644 src/OneScript.StandardLibrary/Security/Tokens/AccessTokenSignAlgorithmEnum.cs create mode 100644 tests/access-token.os diff --git a/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs b/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs index 78472bfbc..d174fc23f 100644 --- a/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs +++ b/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs @@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the using System.Text; using OneScript.Contexts; using OneScript.Exceptions; +using OneScript.Execution; using OneScript.StandardLibrary.Collections; using OneScript.Types; using RegExp = System.Text.RegularExpressions; @@ -130,9 +131,9 @@ public int Timeout /// Строка. Имя файла, в который нужно записать ответ. Необязательный параметр. /// HTTPОтвет. Ответ сервера. [ContextMethod("Получить", "Get")] - public HttpResponseContext Get(HttpRequestContext request, string output = null) + public HttpResponseContext Get(HttpRequestContext request, string output = null, IBslProcess process = null) { - return GetResponse(request, "GET", output); + return GetResponse(request, "GET", output, process); } /// @@ -141,9 +142,9 @@ public HttpResponseContext Get(HttpRequestContext request, string output = null) /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("Записать", "Put")] - public HttpResponseContext Put(HttpRequestContext request) + public HttpResponseContext Put(HttpRequestContext request, IBslProcess process = null) { - return GetResponse(request, "PUT"); + return GetResponse(request, "PUT", null, process); } /// @@ -153,9 +154,9 @@ public HttpResponseContext Put(HttpRequestContext request) /// Строка. Имя файла, в который нужно записать ответ. Необязательный параметр. /// HTTPОтвет. Ответ сервера. [ContextMethod("ОтправитьДляОбработки", "Post")] - public HttpResponseContext Post(HttpRequestContext request, string output = null) + public HttpResponseContext Post(HttpRequestContext request, string output = null, IBslProcess process = null) { - return GetResponse(request, "POST", output); + return GetResponse(request, "POST", output, process); } /// @@ -164,9 +165,9 @@ public HttpResponseContext Post(HttpRequestContext request, string output = null /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("Удалить", "Delete")] - public HttpResponseContext Delete(HttpRequestContext request) + public HttpResponseContext Delete(HttpRequestContext request, IBslProcess process = null) { - return GetResponse(request, "DELETE"); + return GetResponse(request, "DELETE", null, process); } /// @@ -175,9 +176,9 @@ public HttpResponseContext Delete(HttpRequestContext request) /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("Изменить", "Patch")] - public HttpResponseContext Patch(HttpRequestContext request) + public HttpResponseContext Patch(HttpRequestContext request, IBslProcess process = null) { - return GetResponse(request, "PATCH"); + return GetResponse(request, "PATCH", null, process); } /// @@ -186,9 +187,9 @@ public HttpResponseContext Patch(HttpRequestContext request) /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("ПолучитьЗаголовки", "Head")] - public HttpResponseContext Head(HttpRequestContext request) + public HttpResponseContext Head(HttpRequestContext request, IBslProcess process = null) { - return GetResponse(request, "HEAD"); + return GetResponse(request, "HEAD", null, process); } /// @@ -199,9 +200,9 @@ public HttpResponseContext Head(HttpRequestContext request) /// Строка. Имя выходного файла /// HTTPОтвет. Ответ сервера. [ContextMethod("ВызватьHTTPМетод", "CallHTTPMethod")] - public HttpResponseContext Patch(string method, HttpRequestContext request, string output = null) + public HttpResponseContext CallHTTPMethod(string method, HttpRequestContext request, string output = null, IBslProcess process = null) { - return GetResponse(request, method, output); + return GetResponse(request, method, output, process); } private HttpWebRequest CreateRequest(string resource) @@ -335,12 +336,12 @@ private static List ParseRange(string rangeHeader) return range; } - private HttpResponseContext GetResponse(HttpRequestContext request, string method, string output = null) + private HttpResponseContext GetResponse(HttpRequestContext request, string method, string output = null, IBslProcess process = null) { var webRequest = CreateRequest(request.ResourceAddress); webRequest.AllowAutoRedirect = AllowAutoRedirect; webRequest.Method = method; - SetRequestHeaders(request, webRequest); + SetRequestHeaders(request, webRequest, process); if (ContentBodyAllowed(method)) SetRequestBody(request, webRequest); @@ -385,7 +386,7 @@ private static void SetRequestBody(HttpRequestContext request, HttpWebRequest we } } - private static void SetRequestHeaders(HttpRequestContext request, HttpWebRequest webRequest) + private static void SetRequestHeaders(HttpRequestContext request, HttpWebRequest webRequest, IBslProcess process) { foreach (var item in request.Headers.Select(x => x.GetRawValue() as KeyAndValueImpl)) { @@ -483,15 +484,19 @@ private static void SetRequestHeaders(HttpRequestContext request, HttpWebRequest default: webRequest.Headers.Set(key, value); break; - } + } - // fix #1151 - if (webRequest.UserAgent == default) - { - webRequest.UserAgent = $"1Script v${Assembly.GetExecutingAssembly().GetName().Version}"; - } + // fix #1151 + if (webRequest.UserAgent == default) + { + webRequest.UserAgent = $"1Script v${Assembly.GetExecutingAssembly().GetName().Version}"; + } + if (request.AccessToken != null) + { + string token = request.AccessToken.ToString(process); + webRequest.Headers.Set(HttpRequestHeader.Authorization, $"Bearer {token}"); } } diff --git a/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs b/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs index a18a8c6c9..2b45fd57c 100644 --- a/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs +++ b/src/OneScript.StandardLibrary/Http/HttpRequestContext.cs @@ -11,6 +11,7 @@ This Source Code Form is subject to the terms of the using OneScript.Exceptions; using OneScript.StandardLibrary.Binary; using OneScript.StandardLibrary.Collections; +using OneScript.StandardLibrary.Security.Tokens; using OneScript.StandardLibrary.Text; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -44,6 +45,8 @@ private void SetBody(IHttpRequestBody newBody) public Stream Body => _body?.GetDataStream(); + public AccessTokenContext AccessToken { get; private set; } + /// /// Относительный путь к ресурсу на сервере (не включает имя сервера) /// @@ -115,6 +118,16 @@ public GenericStream GetBodyAsStream() return new GenericStream(_body.GetDataStream()); } + /// + /// Добавляет токен доступа к HTTP-запросу. Устанавливает заголовок Authorization. + /// + /// Токен доступа + [ContextMethod("ДобавитьТокенДоступа", "AddAccessToken")] + public void AddAccessToken(AccessTokenContext accessToken) + { + AccessToken = accessToken; + } + [ScriptConstructor(Name = "Формирование неинициализированного объекта")] public static HttpRequestContext Constructor() { diff --git a/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj b/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj index 1c2bbf009..2191c573f 100644 --- a/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj +++ b/src/OneScript.StandardLibrary/OneScript.StandardLibrary.csproj @@ -26,6 +26,7 @@ + diff --git a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs new file mode 100644 index 000000000..517c6b0ae --- /dev/null +++ b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs @@ -0,0 +1,383 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Security.Claims; +using System.Security.Cryptography; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using OneScript.Execution; +using OneScript.Contexts; +using OneScript.StandardLibrary.Collections; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; + +namespace OneScript.StandardLibrary.Security.Tokens +{ + /// + /// Описывает структуру токена доступа в формате JSON Web Token. + /// + [ContextClass("ТокенДоступа", "AccessToken")] + public class AccessTokenContext : AutoContext, IDisposable + { + private int? _lifeTime; + private int? _creationTime; + private string _tokenId; + private string _userMatchingKey; + private string _issuer; + private string _token; + private bool _isSigned = false; + private RSA _rsa; + private ECDsa _ecdsa; + private bool _disposed = false; + + /// + /// Определяет время жизни токена в секундах. + /// Устанавливает значение для ключа 'exp' равное сумме времени создания и времени жизни. + /// + /// Число (Number) + [ContextProperty("ВремяЖизни", "LifeTime")] + public int LifeTime + { + get => _lifeTime ?? 0; + set => _lifeTime = value; + } + + /// + /// Числовое значение времени создания токена доступа в формате UnixTime (количество секунд, прошедших с + /// полуночи 01.01.1970). Соответствует ключам 'iat' и 'nbf'. + /// + /// Число (Number) + [ContextProperty("ВремяСоздания", "CreationTime")] + public int CreationTime + { + get => _creationTime ?? 0; + set => _creationTime = value; + } + + /// + /// Заголовки токена доступа. + /// + /// Соответствие (Map) + [ContextProperty("Заголовки", "Headers")] + public MapImpl Headers { get; set; } + + /// + /// Идентификатор токена. + /// + /// Строка (String) + [ContextProperty("Идентификатор", "ID")] + public string TokenId + { + get => _tokenId ?? ""; + set => _tokenId = value; + } + + /// + /// Ключ сопоставления пользователя. Соответствует ключу 'sub' токена доступа. + /// + /// Строка (String) + [ContextProperty("КлючСопоставленияПользователя", "UserMatchingKey")] + public string UserMatchingKey + { + get => _userMatchingKey ?? ""; + set => _userMatchingKey = value; + } + + /// + /// Полезная нагрузка токена доступа. + /// + /// Соответствие (Map) + [ContextProperty("ПолезнаяНагрузка", "Payload")] + public MapImpl Payload { get; set; } + + /// + /// Массив строк, который содержит идентификаторы получателей токена. Соответствует ключу 'aud'. + /// + /// Массив (Array) + [ContextProperty("Получатели", "Recipients")] + public ArrayImpl Recipients { get; set; } + + /// + /// Идентификатор эмитента, выпустившего токен. Соответствует ключу 'iss'. + /// + /// Строка (String) + [ContextProperty("Эмитент", "Issuer")] + public string Issuer + { + get => _issuer ?? ""; + set => _issuer = value; + } + + /// + /// Добавляет токену доступа подпись по указанному в параметрах алгоритму. + /// + /// Алгоритм подписи токена доступа. + /// Информация о ключе, который используется для формирования подписи в формате PEM. + /// Данный параметр является необязательным только, если не указан алгоритм подписи. + [ContextMethod("Подписать", "Sign")] + public void Sign(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey = "") + { + CreateToken(process, algorithm, secretKey); + _isSigned = true; + } + + private void CreateUnsignedToken(IBslProcess process) + { + CreateToken(process, AccessTokenSignAlgorithmEnum.None); + } + + private void CreateToken(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey = "") + { + var claims = new List(); + + AddStandardClaims(claims); + AddAudienceToClaims(process, claims); + AddPayloadToClaims(process, claims); + + var tokenHandler = new JwtSecurityTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + AdditionalInnerHeaderClaims = GetHeaderClaims(process), + SigningCredentials = GetSigningCredentials(algorithm, secretKey) + }; + + try + { + SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); + _token = tokenHandler.WriteToken(token); + } + catch (Exception ex) + { + throw new SecurityTokenException($"Ошибка при создании токена: {ex.Message}", ex); + } + } + + private void AddStandardClaims(List claims) + { + if(_issuer != null) + claims.Add(new Claim(JwtRegisteredClaimNames.Iss, _issuer)); + + if (CreationTime != 0) + { + claims.Add(new Claim(JwtRegisteredClaimNames.Iat, CreationTime.ToString(), ClaimValueTypes.Integer64)); + claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, CreationTime.ToString(), ClaimValueTypes.Integer64)); + } + + if (CreationTime != 0 || LifeTime != 0) + { + int expires = CreationTime + LifeTime; + claims.Add(new Claim(JwtRegisteredClaimNames.Exp, expires.ToString(), ClaimValueTypes.Integer64)); + } + + if(_tokenId != null) + claims.Add(new Claim(JwtRegisteredClaimNames.Jti, _tokenId)); + + if(_userMatchingKey != null) + claims.Add(new Claim(JwtRegisteredClaimNames.Sub, _userMatchingKey)); + } + + private void AddAudienceToClaims(IBslProcess process, List claims) + { + if (Recipients == null || Recipients.Count() == 0) + return; + + foreach (var recipient in Recipients) + { + claims.Add(new Claim(JwtRegisteredClaimNames.Aud, recipient.AsString(process))); + } + } + + private void AddPayloadToClaims(IBslProcess process, List claims) + { + if (Payload == null || Payload.Count() == 0) + return; + + foreach (var payloadItem in Payload) + { + var key = payloadItem.Key?.AsString(process); + var value = payloadItem.Value?.AsString(process); + + if(!String.IsNullOrEmpty(key) && value != null) + claims.Add(new Claim(key, value)); + } + } + + private Dictionary GetHeaderClaims(IBslProcess process) + { + var headerClaims = new Dictionary(); + + if (Headers == null || Headers.Count() == 0) + return headerClaims; + + foreach (var headerItem in Headers) + { + var key = headerItem.Key.AsString(process); + var value = headerItem.Value.AsString(process); + + if(!String.IsNullOrEmpty(key)) + headerClaims.Add(key, value); + } + + return headerClaims; + } + + private SigningCredentials GetSigningCredentials(AccessTokenSignAlgorithmEnum algorithm, string secretKey) + { + if (algorithm == AccessTokenSignAlgorithmEnum.None) + return null; + + if (string.IsNullOrEmpty(secretKey)) + throw new ArgumentException("Ключ подписи не может быть пустым для выбранного алгоритма"); + + var key = GetSigningKey(algorithm, secretKey); + return new SigningCredentials(key, GetSecurityAlgorithm(algorithm)); + } + + private SecurityKey GetSigningKey(AccessTokenSignAlgorithmEnum algorithm, string secretKey) + { + return algorithm switch + { + // Симметричные алгоритмы (HMAC) + AccessTokenSignAlgorithmEnum.HS256 => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), + AccessTokenSignAlgorithmEnum.HS384 => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), + AccessTokenSignAlgorithmEnum.HS512 => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), + + // Асимметричные алгоритмы (RSA) + AccessTokenSignAlgorithmEnum.RS256 => CreateRsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.RS384 => CreateRsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.RS512 => CreateRsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.PS256 => CreateRsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.PS384 => CreateRsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.PS512 => CreateRsaSecurityKey(secretKey), + + // Эллиптические кривые (ECDSA) + AccessTokenSignAlgorithmEnum.ES256 => CreateEcdsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.ES384 => CreateEcdsaSecurityKey(secretKey), + AccessTokenSignAlgorithmEnum.ES512 => CreateEcdsaSecurityKey(secretKey), + + _ => throw new ArgumentException($"Неподдерживаемый алгоритм для ключа: {algorithm}") + }; + } + + private SecurityKey CreateRsaSecurityKey(string secretKey) + { + _rsa?.Dispose(); + _rsa = RSA.Create(); + try + { + _rsa.ImportFromPem(secretKey); + return new RsaSecurityKey(_rsa); + } + catch (Exception ex) + { + _rsa.Dispose(); + _rsa = null; + throw new ArgumentException($"Ошибка при создании RSA ключа: {ex.Message}", ex); + } + } + + private SecurityKey CreateEcdsaSecurityKey(string secretKey) + { + _ecdsa?.Dispose(); + _ecdsa = ECDsa.Create(); + try + { + _ecdsa.ImportFromPem(secretKey); + return new ECDsaSecurityKey(_ecdsa); + } + catch (Exception ex) + { + _ecdsa.Dispose(); + _ecdsa = null; + throw new ArgumentException($"Ошибка при создании ECDSA ключа: {ex.Message}", ex); + } + } + + private string GetSecurityAlgorithm(AccessTokenSignAlgorithmEnum algorithm) + { + return algorithm switch + { + // HMAC алгоритмы + AccessTokenSignAlgorithmEnum.HS256 => SecurityAlgorithms.HmacSha256, + AccessTokenSignAlgorithmEnum.HS384 => SecurityAlgorithms.HmacSha384, + AccessTokenSignAlgorithmEnum.HS512 => SecurityAlgorithms.HmacSha512, + + // RSA алгоритмы + AccessTokenSignAlgorithmEnum.RS256 => SecurityAlgorithms.RsaSha256, + AccessTokenSignAlgorithmEnum.RS384 => SecurityAlgorithms.RsaSha384, + AccessTokenSignAlgorithmEnum.RS512 => SecurityAlgorithms.RsaSha512, + + // RSA-PSS алгоритмы + AccessTokenSignAlgorithmEnum.PS256 => SecurityAlgorithms.RsaSsaPssSha256, + AccessTokenSignAlgorithmEnum.PS384 => SecurityAlgorithms.RsaSsaPssSha384, + AccessTokenSignAlgorithmEnum.PS512 => SecurityAlgorithms.RsaSsaPssSha512, + + // ECDSA алгоритмы + AccessTokenSignAlgorithmEnum.ES256 => SecurityAlgorithms.EcdsaSha256, + AccessTokenSignAlgorithmEnum.ES384 => SecurityAlgorithms.EcdsaSha384, + AccessTokenSignAlgorithmEnum.ES512 => SecurityAlgorithms.EcdsaSha512, + + _ => throw new ArgumentException($"Неподдерживаемый алгоритм: {algorithm}") + }; + } + + [ScriptConstructor(Name = "По умолчанию")] + public static AccessTokenContext Constructor() + { + return new AccessTokenContext(); + } + + [ScriptConstructor(Name = "По заголовкам и полезной нагрузке")] + public static AccessTokenContext Constructor(MapImpl headers, MapImpl payload) + { + return new AccessTokenContext(headers, payload); + } + + public override string ToString(IBslProcess process) + { + if (_isSigned) + return _token; + + CreateUnsignedToken(process); + + return _token; + } + + private AccessTokenContext() + { + Headers = new MapImpl(); + Payload = new MapImpl(); + Recipients = new ArrayImpl(); + } + + private AccessTokenContext(MapImpl headers, MapImpl payload) : this() + { + Headers = headers; + Payload = payload; + } + + public void Dispose() + { + if (_disposed) + return; + + _rsa?.Dispose(); + _ecdsa?.Dispose(); + _rsa = null; + _ecdsa = null; + + _disposed = true; + } + } +} diff --git a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenSignAlgorithmEnum.cs b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenSignAlgorithmEnum.cs new file mode 100644 index 000000000..6ca4424a7 --- /dev/null +++ b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenSignAlgorithmEnum.cs @@ -0,0 +1,57 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using OneScript.Contexts.Enums; + +namespace OneScript.StandardLibrary.Security.Tokens +{ + /// + /// Алгоритмы подписи токена доступа. + /// + [EnumerationType("АлгоритмПодписиТокенаДоступа", "AccessTokenSignAlgorithm")] + public enum AccessTokenSignAlgorithmEnum + { + [EnumValue("ES256")] + ES256, + + [EnumValue("ES384")] + ES384, + + [EnumValue("ES512")] + ES512, + + [EnumValue("HS256")] + HS256, + + [EnumValue("HS384")] + HS384, + + [EnumValue("HS512")] + HS512, + + [EnumValue("PS256")] + PS256, + + [EnumValue("PS384")] + PS384, + + [EnumValue("PS512")] + PS512, + + [EnumValue("RS256")] + RS256, + + [EnumValue("RS384")] + RS384, + + [EnumValue("RS512")] + RS512, + + [EnumValue("Нет", "None")] + None + } +} diff --git a/tests/access-token.os b/tests/access-token.os new file mode 100644 index 000000000..624501116 --- /dev/null +++ b/tests/access-token.os @@ -0,0 +1,210 @@ +Перем юТест; + +Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт + юТест = ЮнитТестирование; + + ВсеТесты = Новый Массив; + + ВсеТесты.Добавить("ТестДолжен_ПроверитьКонструкторПоУмолчанию"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьКонструкторСЗаголовкамиИПолезнойНагрузкой"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаСКлючом"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаБезКлюча"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьОшибкуПриПодписиБезКлюча"); + + Возврат ВсеТесты; +КонецФункции + +Процедура ТестДолжен_ПроверитьКонструкторПоУмолчанию() Экспорт + + Токен = Новый ТокенДоступа(); + + юТест.ПроверитьРавенство(Токен.ВремяЖизни, 0); + юТест.ПроверитьРавенство(Токен.ВремяСоздания, 0); + юТест.ПроверитьРавенство(Токен.Идентификатор, ""); + юТест.ПроверитьРавенство(Токен.КлючСопоставленияПользователя, ""); + юТест.ПроверитьРавенство(Токен.Эмитент, ""); + юТест.ПроверитьРавенство(Токен.Заголовки.Количество(), 0); + юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка.Количество(), 0); + юТест.ПроверитьРавенство(Токен.Получатели.Количество(), 0); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьКонструкторСЗаголовкамиИПолезнойНагрузкой() Экспорт + + Заголовки = Новый Соответствие; + Заголовки["typ"] = "JWT"; + Заголовки["any"] = "some"; + + ПолезнаяНагрузка = Новый Соответствие; + ПолезнаяНагрузка["sub"] = "user123"; + ПолезнаяНагрузка["name"] = "Test User"; + + Токен = Новый ТокенДоступа(Заголовки, ПолезнаяНагрузка); + + юТест.ПроверитьРавенство(Токен.Заголовки.Количество(), 2); + юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка.Количество(), 2); + юТест.ПроверитьРавенство(Токен.Заголовки["typ"], "JWT"); + юТест.ПроверитьРавенство(Токен.Заголовки["any"], "some"); + юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка["sub"], "user123"); + юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка["name"], "Test User"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьГенерациюТокенаСКлючом() Экспорт + + ПриватныйКлючECDSA = ПриватныйКлючECDSA(); + ПриватныйКлючRSA = ПриватныйКлючRSA(); + + ТестовыеСлучаи = Новый Соответствие(); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.ES256, ПриватныйКлючECDSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.ES384, ПриватныйКлючECDSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.ES512, ПриватныйКлючECDSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.HS256, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.HS384, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.HS512, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.PS256, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.PS384, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.PS512, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.RS256, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.RS384, ПриватныйКлючRSA); + ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.RS512, ПриватныйКлючRSA); + + Токен = ТестовыйТокенДоступа(); + ОжидаемыеЧастиТокена = ОжидаемыеЧастиТестовогоТокена(); + + Для Каждого ТестовыйСлучай Из ТестовыеСлучаи Цикл + Алгоритм = ТестовыйСлучай.Ключ; + ПриватныйКлюч = ТестовыйСлучай.Значение; + + Токен.Подписать(Алгоритм, ПриватныйКлюч); + + РазобранныйТокен = РазобратьJWT(Токен); + + юТест.ПроверитьРавенство(РазобранныйТокен.Заголовки, СтрШаблон(ОжидаемыеЧастиТокена.Заголовки, Алгоритм), "Заголовки для " + Алгоритм); + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, ОжидаемыеЧастиТокена.ПолезнаяНагрузка, "Полезная нагрузка для " + Алгоритм); + юТест.ПроверитьЗаполненность(РазобранныйТокен.Сигнатура, "Сигнатура для " + Алгоритм); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьГенерациюТокенаБезКлюча() Экспорт + + Токен = ТестовыйТокенДоступа(); + Токен.Подписать(АлгоритмПодписиТокенаДоступа.Нет); + + РазобранныйТокен = РазобратьJWT(Токен); + ОжидаемыеЧастиТокена = ОжидаемыеЧастиТестовогоТокена(); + + юТест.ПроверитьРавенство(РазобранныйТокен.Заголовки, СтрШаблон(ОжидаемыеЧастиТокена.Заголовки, "none"), "Заголовки"); + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, ОжидаемыеЧастиТокена.ПолезнаяНагрузка, "Полезная нагрузка"); + юТест.ПроверитьНеЗаполненность(РазобранныйТокен.Сигнатура, "Сигнатура"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьОшибкуПриПодписиБезКлюча() Экспорт + юТест.ПроверитьКодСОшибкой( + "Токен = Новый ТокенДоступа(); + |Токен.Подписать(АлгоритмПодписиТокенаДоступа.ES256)", + "Ключ подписи не может быть пустым для выбранного алгоритма", + "Должна была возникнуть ошибка при подписи без ключа" + ); +КонецПроцедуры + +Функция ТестовыйТокенДоступа() + + Токен = Новый ТокенДоступа(); + Токен.ВремяЖизни = 3600; + Токен.ВремяСоздания = 1640995200; // 2022-01-01 00:00:00 UTC + Токен.Идентификатор = "unique-token-id"; + Токен.КлючСопоставленияПользователя = "john.doe@example.com"; + Токен.Эмитент = "https://auth.example.com"; + + Токен.Заголовки["typ"] = "JWT"; + + Токен.ПолезнаяНагрузка["role"] = "admin"; + Токен.ПолезнаяНагрузка["permissions"] = "read,write,delete"; + + Токен.Получатели.Добавить("api.example.com"); + Токен.Получатели.Добавить("mobile.app"); + + Возврат Токен; + +КонецФункции + +Функция ОжидаемыеЧастиТестовогоТокена() + + ОжидаемыеЗаголовки = "{""alg"":""%1"",""typ"":""JWT""}"; + ОжидаемаяПолезнаяНагрузка = "{""iss"":""https://auth.example.com"",""iat"":1640995200,""nbf"":1640995200,""exp"":1640998800,""jti"":""unique-token-id"",""sub"":""john.doe@example.com"",""aud"":[""api.example.com"",""mobile.app""],""role"":""admin"",""permissions"":""read,write,delete""}"; + + Возврат Новый Структура("Заголовки, ПолезнаяНагрузка", ОжидаемыеЗаголовки, ОжидаемаяПолезнаяНагрузка); + +КонецФункции + +Функция РазобратьJWT(Токен) + + Части = СтрРазделить(Токен, "."); + юТест.ПроверитьРавенство(Части.Количество(), 3, "Количество частей"); + + Результат = Новый Структура("Заголовки, ПолезнаяНагрузка, Сигнатура"); + Результат.Заголовки = ПолучитьСтрокуИзДвоичныхДанных(Base64Значение(НормализоватьBase64(Части[0]))); + Результат.ПолезнаяНагрузка = ПолучитьСтрокуИзДвоичныхДанных(Base64Значение(НормализоватьBase64(Части[1]))); + Результат.Сигнатура = Части[2]; + + Возврат Результат; + +КонецФункции + +Функция НормализоватьBase64(Значение) + Длина = СтрДлина(Значение); + Остаток = Длина % 4; + Паддинг = ""; + Если Остаток = 2 Тогда + Паддинг = "=="; + ИначеЕсли Остаток = 3 Тогда + Паддинг = "="; + КонецЕсли; + Возврат Значение + Паддинг; +КонецФункции + +// 2048 bits, PKCS#8 +Функция ПриватныйКлючRSA() Экспорт + Возврат " + |-----BEGIN PRIVATE KEY----- + |MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT4Ws/kRkM92gs + |rljP85+ZaBoE/aJa5yBoWOJI5notYGDwAU66WofBunLIlUNn/YuETvs12mCCClBX + |ODurlwi+wLs5HyXf1gqNzVP4JN5QFUpqd2RQrUZLqBoONjZAok9P3oAw4JuJDnBm + |JODFLQjbCUOvorxR33iCPDfflvSa8kIxkQ3hA6zB7vu0j5s8HGCXczRI0pSblAH3 + |GEPSPp8D4IBmVKi90lQjCVVlCMY/hVkecqjc2hhfsh2cF/R2Q3NptNSLtw+lEMOU + |uUQtgVnULhTYDRaRJ9HripxQvQ2Se/BnozFVPfKokouh6f0ti9b4vWSDN3EjuMEu + |wf+ttUPhAgMBAAECggEAZRT3lzraMbfHmIsQIAz6MgUri+/HE3Xa/BiNjKEmMHAp + |ssRiCRWqqCyHGz6UFbgErsrCAhykTR85rXS9rNuPWbwp/bCh1e/bxPPuCrdA0uLK + |vB2iD1dxrnNYZBCAYwvPRNvlKgPNrRCWmetpTL4syHP7tUl6ikhDelopIwdvaZ1W + |cQeDq6QqkzOcJFdXHIHxZ2qbnjJoinyfPzmA98Ijdi8ynjIqKxj0f+0r567nxe/q + |Z4FwbPi7bRSmfMsTCM0SagwMuQoqvB6iktrZEYYUJmcN7V/gjN1viqVkjudk/CC5 + |RgMBp7yhrt3SP2oErAaAlTWldRVJ8qquJSpXkBhTxQKBgQDM8S9WIZgl2HwWsK9c + |C2JTC0peB8W1B3rM5IYvrCN0uB/P/KZ/Igwkla3wsOhy2SDPBYqzP/4OPzz4atOL + |PFDdBGrIQgseSajPp7nuD91IjI9KFXUT1EUBbKhWmATZwQqCOlHtSoi4zl+dJWgr + |dVzQxykh3ME85mIU3cxWNxk+swKBgQC4uPSsE9aJ5JK5LCfpcsVYeeatko72rA2t + |N/aPActklWwkR/tvJ43jG5RwKwbY4N2V6a5Tz5xQIe550vvBd56ku6tmEJDLL+jc + |QLh5lajV3JJq+tazdyZkztaSwOucuZve9R+eYAPM3uqYO8gAJSrJQQwlAoJkrWk6 + |zoISqvk9GwKBgFJQsVWkCqtwx26JqvWKcQSv7T/VWVi711wCkc8GEfuolMaCURGR + |SFVNdV7Of57ZjS75p9sVYeKxDbktlyg+orATPRyQQkz5Av/c+3YeEyA3rLnx4mOD + |h+/ph7e4OYKI4cKq0AtCO6YW5hqFUtDZw9zrkZ7TPx5J3q1I5PDgEpi3AoGAYokO + |wwfmBjJubeZ299loGdUUzvwB1OVcek0C8a/kXigywnu/TMDNuBrLKLZa9a+lo8U+ + |X6i8WiZvfn6kGsSDVJ5jRJOLmUdaMLs5bGn/4OzDEdvyUVM6oMWQwkG0rSp5Ugpx + |rlrLTCqQ+1nQSvuIgkh3gPqAsdGq81qbGfgn8YcCgYEAjodWERs0eCSMNGIshss4 + |/GN50lbOiI0+ke99160kT/hpep1WCEpXBfCeXLBamcG36Uk8c34DrxJ2Uj8LC895 + |/tB4HFgbQ52VxQdQCNGcs9hOxPuLlXlSdKNuplvfAkg2JhMrES7KtAxOBAcK77kx + |JzEREQEFN8CmwDvSpw8Mz9k= + |-----END PRIVATE KEY-----"; +КонецФункции + +// secp256r1 +Функция ПриватныйКлючECDSA() + Возврат " + |-----BEGIN PRIVATE KEY----- + |MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZYdbovRCqelX1FPY + |EE8VAbDJ/cT99z8r8ZisRgftCt6hRANCAASFDFrXFeOV0rYTDuIDJHKUdwqc7Z3z + |gCxaeTCRJ+KBCw7AsGearOWLn67Y424YAx73wr3T9BIrQ3yGgPXZmzUR + |-----END PRIVATE KEY-----"; +КонецФункции \ No newline at end of file diff --git a/tests/http.os b/tests/http.os index e430112e5..5849048d6 100644 --- a/tests/http.os +++ b/tests/http.os @@ -36,6 +36,8 @@ ВсеТесты.Добавить("ТестДолженПроверитьЧтоМожноЗадатьТелоЗапросаСПомощьюПотока"); ВсеТесты.Добавить("ТестДолженПроверитьЧтоМетодыБезТелаПриУстановленномТелеУспешноВыполняются"); + + ВсеТесты.Добавить("ТестДолженПроверитьПередачуТокенаЧерезДобавитьТокенДоступа"); Возврат ВсеТесты; КонецФункции @@ -369,6 +371,22 @@ КонецПроцедуры +Процедура ТестДолженПроверитьПередачуТокенаЧерезДобавитьТокенДоступа() Экспорт + + Токен = Новый ТокенДоступа(); + + Запрос = Новый HttpЗапрос("/get"); + Запрос.ДобавитьТокенДоступа(Токен); + + Соединение = Новый HttpСоединение(мАдресРесурса); + Ответ = Соединение.ВызватьHTTPМетод("GET", Запрос); + ТелоОтвета = Ответ.ПолучитьТелоКакСтроку(); + + юТест.ПроверитьРавенство(200, Ответ.КодСостояния); + юТест.ПроверитьВхождение(JsonВОбъект(ТелоОтвета)["headers"]["Authorization"], "Bearer " + Токен); + +КонецПроцедуры + Функция JsonВОбъект(Json) ЧтениеJSON = Новый ЧтениеJSON; From 06dfd39366a00afad69f6e8f2e4eb905a42a6927 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 19 Aug 2025 20:36:22 +0300 Subject: [PATCH 2/7] =?UTF-8?q?=D0=9E=D0=B1=D1=8F=D0=B7=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Http/HttpConnectionContext.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs b/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs index d174fc23f..a55b77dfe 100644 --- a/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs +++ b/src/OneScript.StandardLibrary/Http/HttpConnectionContext.cs @@ -131,9 +131,9 @@ public int Timeout /// Строка. Имя файла, в который нужно записать ответ. Необязательный параметр. /// HTTPОтвет. Ответ сервера. [ContextMethod("Получить", "Get")] - public HttpResponseContext Get(HttpRequestContext request, string output = null, IBslProcess process = null) + public HttpResponseContext Get(IBslProcess process, HttpRequestContext request, string output = null) { - return GetResponse(request, "GET", output, process); + return GetResponse(process, request, "GET", output); } /// @@ -142,9 +142,9 @@ public HttpResponseContext Get(HttpRequestContext request, string output = null, /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("Записать", "Put")] - public HttpResponseContext Put(HttpRequestContext request, IBslProcess process = null) + public HttpResponseContext Put(IBslProcess process, HttpRequestContext request) { - return GetResponse(request, "PUT", null, process); + return GetResponse(process, request, "PUT", null); } /// @@ -154,9 +154,9 @@ public HttpResponseContext Put(HttpRequestContext request, IBslProcess process = /// Строка. Имя файла, в который нужно записать ответ. Необязательный параметр. /// HTTPОтвет. Ответ сервера. [ContextMethod("ОтправитьДляОбработки", "Post")] - public HttpResponseContext Post(HttpRequestContext request, string output = null, IBslProcess process = null) + public HttpResponseContext Post(IBslProcess process, HttpRequestContext request, string output = null) { - return GetResponse(request, "POST", output, process); + return GetResponse(process, request, "POST", output); } /// @@ -165,9 +165,9 @@ public HttpResponseContext Post(HttpRequestContext request, string output = null /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("Удалить", "Delete")] - public HttpResponseContext Delete(HttpRequestContext request, IBslProcess process = null) + public HttpResponseContext Delete(IBslProcess process, HttpRequestContext request) { - return GetResponse(request, "DELETE", null, process); + return GetResponse(process, request, "DELETE"); } /// @@ -176,9 +176,9 @@ public HttpResponseContext Delete(HttpRequestContext request, IBslProcess proces /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("Изменить", "Patch")] - public HttpResponseContext Patch(HttpRequestContext request, IBslProcess process = null) + public HttpResponseContext Patch(IBslProcess process, HttpRequestContext request) { - return GetResponse(request, "PATCH", null, process); + return GetResponse(process, request, "PATCH"); } /// @@ -187,9 +187,9 @@ public HttpResponseContext Patch(HttpRequestContext request, IBslProcess process /// HTTPЗапрос. Данные и заголовки запроса http /// HTTPОтвет. Ответ сервера. [ContextMethod("ПолучитьЗаголовки", "Head")] - public HttpResponseContext Head(HttpRequestContext request, IBslProcess process = null) + public HttpResponseContext Head(IBslProcess process, HttpRequestContext request) { - return GetResponse(request, "HEAD", null, process); + return GetResponse(process, request, "HEAD"); } /// @@ -200,9 +200,9 @@ public HttpResponseContext Head(HttpRequestContext request, IBslProcess process /// Строка. Имя выходного файла /// HTTPОтвет. Ответ сервера. [ContextMethod("ВызватьHTTPМетод", "CallHTTPMethod")] - public HttpResponseContext CallHTTPMethod(string method, HttpRequestContext request, string output = null, IBslProcess process = null) + public HttpResponseContext CallHTTPMethod(IBslProcess process, string method, HttpRequestContext request, string output = null) { - return GetResponse(request, method, output, process); + return GetResponse(process, request, method, output); } private HttpWebRequest CreateRequest(string resource) @@ -336,7 +336,7 @@ private static List ParseRange(string rangeHeader) return range; } - private HttpResponseContext GetResponse(HttpRequestContext request, string method, string output = null, IBslProcess process = null) + private HttpResponseContext GetResponse(IBslProcess process, HttpRequestContext request, string method, string output = null) { var webRequest = CreateRequest(request.ResourceAddress); webRequest.AllowAutoRedirect = AllowAutoRedirect; From 165b2ea92b2da051854c8c0e2a16b1f867086d43 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 20 Aug 2025 01:18:58 +0300 Subject: [PATCH 3/7] =?UTF-8?q?=D0=98=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20Jwt=20=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=BE=D0=B2.=20=D0=9F=D0=BE=D0=B4=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=BA=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B8=D0=B7=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Security/Tokens/AccessTokenContext.cs | 170 ++++++++++++------ tests/access-token.os | 67 ++++++- 2 files changed, 178 insertions(+), 59 deletions(-) diff --git a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs index 517c6b0ae..5a86ec0b9 100644 --- a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs +++ b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs @@ -8,7 +8,6 @@ This Source Code Form is subject to the terms of the using System; using System.Collections.Generic; using System.Text; -using System.Security.Claims; using System.Security.Cryptography; using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Tokens; @@ -135,101 +134,124 @@ private void CreateUnsignedToken(IBslProcess process) private void CreateToken(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey = "") { - var claims = new List(); - - AddStandardClaims(claims); - AddAudienceToClaims(process, claims); - AddPayloadToClaims(process, claims); - - var tokenHandler = new JwtSecurityTokenHandler - { - SetDefaultTimesOnTokenCreation = false - }; - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(claims), - AdditionalInnerHeaderClaims = GetHeaderClaims(process), - SigningCredentials = GetSigningCredentials(algorithm, secretKey) - }; - try { - SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); - _token = tokenHandler.WriteToken(token); + var header = CreateJwtHeader(process, algorithm, secretKey); + var payload = CreateJwtPayload(process); + var jwtToken = new JwtSecurityToken(header, payload); + + var tokenHandler = new JwtSecurityTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + + _token = tokenHandler.WriteToken(jwtToken); } catch (Exception ex) { throw new SecurityTokenException($"Ошибка при создании токена: {ex.Message}", ex); } } + + private JwtHeader CreateJwtHeader(IBslProcess process, AccessTokenSignAlgorithmEnum algorithm, string secretKey) + { + JwtHeader header; - private void AddStandardClaims(List claims) + if (algorithm == AccessTokenSignAlgorithmEnum.None) + { + header = new JwtHeader(); + header["alg"] = "none"; + } + else + { + var signingCredentials = GetSigningCredentials(algorithm, secretKey); + header = new JwtHeader(signingCredentials); + } + + if (Headers != null) + { + foreach (var headerItem in Headers) + { + var key = headerItem.Key.AsString(process); + var value = headerItem.Value.AsString(process); + + if(!String.IsNullOrEmpty(key)) + header[key] = value; + } + } + + return header; + } + + private JwtPayload CreateJwtPayload(IBslProcess process) + { + var payload = new JwtPayload(); + + AddStandardClaimsToPayload(payload); + AddAudienceToPayload(process, payload); + AddCustomClaimsToPayload(process, payload); + + return payload; + } + + private void AddStandardClaimsToPayload(JwtPayload payload) { - if(_issuer != null) - claims.Add(new Claim(JwtRegisteredClaimNames.Iss, _issuer)); + if (_issuer != null) + payload[JwtRegisteredClaimNames.Iss] = _issuer; if (CreationTime != 0) { - claims.Add(new Claim(JwtRegisteredClaimNames.Iat, CreationTime.ToString(), ClaimValueTypes.Integer64)); - claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, CreationTime.ToString(), ClaimValueTypes.Integer64)); + payload[JwtRegisteredClaimNames.Iat] = CreationTime; + payload[JwtRegisteredClaimNames.Nbf] = CreationTime; } if (CreationTime != 0 || LifeTime != 0) { int expires = CreationTime + LifeTime; - claims.Add(new Claim(JwtRegisteredClaimNames.Exp, expires.ToString(), ClaimValueTypes.Integer64)); + payload[JwtRegisteredClaimNames.Exp] = expires; } - - if(_tokenId != null) - claims.Add(new Claim(JwtRegisteredClaimNames.Jti, _tokenId)); + + if (_tokenId != null) + payload[JwtRegisteredClaimNames.Jti] = _tokenId; if(_userMatchingKey != null) - claims.Add(new Claim(JwtRegisteredClaimNames.Sub, _userMatchingKey)); + payload[JwtRegisteredClaimNames.Sub] = _userMatchingKey; } - private void AddAudienceToClaims(IBslProcess process, List claims) + private void AddAudienceToPayload(IBslProcess process, JwtPayload payload) { if (Recipients == null || Recipients.Count() == 0) return; - - foreach (var recipient in Recipients) + + if (Recipients.Count() == 1) { - claims.Add(new Claim(JwtRegisteredClaimNames.Aud, recipient.AsString(process))); + payload[JwtRegisteredClaimNames.Aud] = Recipients[0].AsString(process); } - } - - private void AddPayloadToClaims(IBslProcess process, List claims) - { - if (Payload == null || Payload.Count() == 0) - return; - - foreach (var payloadItem in Payload) + else { - var key = payloadItem.Key?.AsString(process); - var value = payloadItem.Value?.AsString(process); + var recipientsStrings = new List(); + foreach (var recipient in Recipients) + { + recipientsStrings.Add(recipient.AsString(process)); + } - if(!String.IsNullOrEmpty(key) && value != null) - claims.Add(new Claim(key, value)); + payload[JwtRegisteredClaimNames.Aud] = recipientsStrings; } } - private Dictionary GetHeaderClaims(IBslProcess process) + private void AddCustomClaimsToPayload(IBslProcess process, JwtPayload payload) { - var headerClaims = new Dictionary(); - - if (Headers == null || Headers.Count() == 0) - return headerClaims; + if (Payload == null || Payload.Count() == 0) + return; - foreach (var headerItem in Headers) + foreach (var payloadItem in Payload) { - var key = headerItem.Key.AsString(process); - var value = headerItem.Value.AsString(process); + var key = payloadItem.Key?.AsString(process); + var value = ConvertToClrObject(process, payloadItem.Value); - if(!String.IsNullOrEmpty(key)) - headerClaims.Add(key, value); + if(!String.IsNullOrEmpty(key) && value != null) + payload[key] = value; } - - return headerClaims; } private SigningCredentials GetSigningCredentials(AccessTokenSignAlgorithmEnum algorithm, string secretKey) @@ -332,6 +354,38 @@ private string GetSecurityAlgorithm(AccessTokenSignAlgorithmEnum algorithm) }; } + private object ConvertToClrObject(IBslProcess process, IValue value) + { + if (value == null) + return null; + + switch (value) + { + case ArrayImpl array: + case FixedArrayImpl fixedArray: + var list = new List(); + foreach (var item in (IEnumerable)value) + { + list.Add(ConvertToClrObject(process, item)); + } + return list; + case StructureImpl structure: + case FixedStructureImpl fixedStructure: + case MapImpl map: + case FixedMapImpl fixedMap: + var dict = new Dictionary(); + foreach (var item in (IEnumerable)value) + { + var key = item.Key.AsString(process); + dict[key] = ConvertToClrObject(process, item.Value); + } + return dict; + default: + var unwarpedValue = value.UnwrapToClrObject() ?? ""; + return unwarpedValue.GetType().IsValueType ? unwarpedValue : value.AsString(process); + } + } + [ScriptConstructor(Name = "По умолчанию")] public static AccessTokenContext Constructor() { diff --git a/tests/access-token.os b/tests/access-token.os index 624501116..a8c2f1078 100644 --- a/tests/access-token.os +++ b/tests/access-token.os @@ -10,6 +10,9 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаСКлючом"); ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаБезКлюча"); ВсеТесты.Добавить("ТестДолжен_ПроверитьОшибкуПриПодписиБезКлюча"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаСОднимПолучателем"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаСДвумяПолучателями"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаCРазличнымиТипамиЗначений"); Возврат ВсеТесты; КонецФункции @@ -101,6 +104,62 @@ КонецПроцедуры +Процедура ТестДолжен_ПроверитьГенерациюТокенаСОднимПолучателем() Экспорт + + Токен = Новый ТокенДоступа(); + Токен.Получатели.Добавить("mobile.app"); + + РазобранныйТокен = РазобратьJWT(Токен); + ОжидаемыеЧастиТокена = ОжидаемыеЧастиТестовогоТокена(); + + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{""aud"":""mobile.app""}"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьГенерациюТокенаСДвумяПолучателями() Экспорт + + Токен = Новый ТокенДоступа(); + Токен.Получатели.Добавить("api.example.com"); + Токен.Получатели.Добавить("mobile.app"); + + РазобранныйТокен = РазобратьJWT(Токен); + ОжидаемыеЧастиТокена = ОжидаемыеЧастиТестовогоТокена(); + + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{""aud"":[""api.example.com"",""mobile.app""]}"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьГенерациюТокенаCРазличнымиТипамиЗначений() Экспорт + + ОжидаемоеЗначение = "{""Число"":1000.10,""Булево"":true,""Дата"":""2001-01-01T17:20:20.0000000Z"",""Нуль"":"""",""Неопределено"":"""",""Массив"":[100,{""Ключ1"":""Значение1""},{""Ключ2"":""Значение2"",""Структура"":{""Ключ1"":""Значение1""}}],""Структура"":{""Ключ1"":""Значение1""},""Соответствие"":{""Ключ2"":""Значение2"",""Структура"":{""Ключ1"":""Значение1""}}}"; + + Структура = Новый Структура("Ключ1", "Значение1"); + + Соответствие = Новый Соответствие(); + Соответствие.Вставить("Ключ2", "Значение2"); + Соответствие.Вставить("Структура", Структура); + + Массив = Новый Массив(); + Массив.Добавить(100); + Массив.Добавить(Структура); + Массив.Добавить(Соответствие); + + Токен = Новый ТокенДоступа(); + Токен.ПолезнаяНагрузка.Вставить("Число", 1000.10); + Токен.ПолезнаяНагрузка.Вставить("Булево", Истина); + Токен.ПолезнаяНагрузка.Вставить("Дата", Дата(2001, 1, 1, 20, 20, 20)); + Токен.ПолезнаяНагрузка.Вставить("Нуль", null); + Токен.ПолезнаяНагрузка.Вставить("Неопределено", Неопределено); + Токен.ПолезнаяНагрузка.Вставить("Массив", Массив); + Токен.ПолезнаяНагрузка.Вставить("Структура", Структура); + Токен.ПолезнаяНагрузка.Вставить("Соответствие", Соответствие); + + РазобранныйТокен = РазобратьJWT(Токен); + + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, ОжидаемоеЗначение); + +КонецПроцедуры + Процедура ТестДолжен_ПроверитьОшибкуПриПодписиБезКлюча() Экспорт юТест.ПроверитьКодСОшибкой( "Токен = Новый ТокенДоступа(); @@ -154,15 +213,21 @@ КонецФункции -Функция НормализоватьBase64(Значение) +Функция НормализоватьBase64(Знач Значение) + + Значение = СтрЗаменить(Значение, "-", "+"); + Значение = СтрЗаменить(Значение, "_", "/"); + Длина = СтрДлина(Значение); Остаток = Длина % 4; Паддинг = ""; + Если Остаток = 2 Тогда Паддинг = "=="; ИначеЕсли Остаток = 3 Тогда Паддинг = "="; КонецЕсли; + Возврат Значение + Паддинг; КонецФункции From b620f469da3927c3edfa3251874b683bd216fb14 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 25 Aug 2025 20:51:01 +0300 Subject: [PATCH 4/7] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=82=D0=B8=D0=BF=D0=B0=20=D1=83=20=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=B9=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2,=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B7=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=BD=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D1=82=D0=B8=D0=BF=D0=B0=20=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Security/Tokens/AccessTokenContext.cs | 19 +++++++++++++++---- tests/access-token.os | 2 -- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs index 5a86ec0b9..f0dbc1395 100644 --- a/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs +++ b/src/OneScript.StandardLibrary/Security/Tokens/AccessTokenContext.cs @@ -11,9 +11,11 @@ This Source Code Form is subject to the terms of the using System.Security.Cryptography; using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Tokens; -using OneScript.Execution; using OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.Execution; using OneScript.StandardLibrary.Collections; +using OneScript.Types; using ScriptEngine.Machine; using ScriptEngine.Machine.Contexts; @@ -172,7 +174,10 @@ private JwtHeader CreateJwtHeader(IBslProcess process, AccessTokenSignAlgorithmE { foreach (var headerItem in Headers) { - var key = headerItem.Key.AsString(process); + if (headerItem.Key.SystemType != BasicTypes.String) + throw RuntimeException.InvalidArgumentType(); + + var key = headerItem.Key.ToString(); var value = headerItem.Value.AsString(process); if(!String.IsNullOrEmpty(key)) @@ -232,7 +237,10 @@ private void AddAudienceToPayload(IBslProcess process, JwtPayload payload) var recipientsStrings = new List(); foreach (var recipient in Recipients) { - recipientsStrings.Add(recipient.AsString(process)); + if (recipient.SystemType != BasicTypes.String) + throw RuntimeException.InvalidArgumentType(); + + recipientsStrings.Add(recipient.ToString()); } payload[JwtRegisteredClaimNames.Aud] = recipientsStrings; @@ -246,7 +254,10 @@ private void AddCustomClaimsToPayload(IBslProcess process, JwtPayload payload) foreach (var payloadItem in Payload) { - var key = payloadItem.Key?.AsString(process); + if (payloadItem.Key.SystemType != BasicTypes.String) + throw RuntimeException.InvalidArgumentType(); + + var key = payloadItem.Key.ToString(); var value = ConvertToClrObject(process, payloadItem.Value); if(!String.IsNullOrEmpty(key) && value != null) diff --git a/tests/access-token.os b/tests/access-token.os index a8c2f1078..72d604fa3 100644 --- a/tests/access-token.os +++ b/tests/access-token.os @@ -110,7 +110,6 @@ Токен.Получатели.Добавить("mobile.app"); РазобранныйТокен = РазобратьJWT(Токен); - ОжидаемыеЧастиТокена = ОжидаемыеЧастиТестовогоТокена(); юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{""aud"":""mobile.app""}"); @@ -123,7 +122,6 @@ Токен.Получатели.Добавить("mobile.app"); РазобранныйТокен = РазобратьJWT(Токен); - ОжидаемыеЧастиТокена = ОжидаемыеЧастиТестовогоТокена(); юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{""aud"":[""api.example.com"",""mobile.app""]}"); From 0f2efbd5097a8d5948d0674f0c4ea687ff9dbaa7 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Mon, 8 Dec 2025 23:59:28 +0300 Subject: [PATCH 5/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/access-token.os | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/access-token.os b/tests/access-token.os index 72d604fa3..c411ba5af 100644 --- a/tests/access-token.os +++ b/tests/access-token.os @@ -7,6 +7,7 @@ ВсеТесты.Добавить("ТестДолжен_ПроверитьКонструкторПоУмолчанию"); ВсеТесты.Добавить("ТестДолжен_ПроверитьКонструкторСЗаголовкамиИПолезнойНагрузкой"); + ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокена"); ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаСКлючом"); ВсеТесты.Добавить("ТестДолжен_ПроверитьГенерациюТокенаБезКлюча"); ВсеТесты.Добавить("ТестДолжен_ПроверитьОшибкуПриПодписиБезКлюча"); @@ -30,6 +31,10 @@ юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка.Количество(), 0); юТест.ПроверитьРавенство(Токен.Получатели.Количество(), 0); + РазобранныйТокен = РазобратьJWT(Токен); + юТест.ПроверитьРавенство(РазобранныйТокен.Заголовки, "{""alg"":""none""}", "Заголовки"); + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{}", "Полезная нагрузка"); + КонецПроцедуры Процедура ТестДолжен_ПроверитьКонструкторСЗаголовкамиИПолезнойНагрузкой() Экспорт @@ -43,6 +48,7 @@ ПолезнаяНагрузка["name"] = "Test User"; Токен = Новый ТокенДоступа(Заголовки, ПолезнаяНагрузка); + РазобранныйТокен = РазобратьJWT(Токен); юТест.ПроверитьРавенство(Токен.Заголовки.Количество(), 2); юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка.Количество(), 2); @@ -50,6 +56,32 @@ юТест.ПроверитьРавенство(Токен.Заголовки["any"], "some"); юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка["sub"], "user123"); юТест.ПроверитьРавенство(Токен.ПолезнаяНагрузка["name"], "Test User"); + юТест.ПроверитьРавенство(РазобранныйТокен.Заголовки, "{""alg"":""none"",""typ"":""JWT"",""any"":""some""}", "Заголовки"); + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{""sub"":""user123"",""name"":""Test User""}", "Полезная нагрузка"); + +КонецПроцедуры + +Процедура ТестДолжен_ПроверитьГенерациюТокена() Экспорт + + Токен = Новый ТокенДоступа(); + Токен.ВремяЖизни = 3600; + Токен.ВремяСоздания = 1640995200; // 2022-01-01 00:00:00 UTC + Токен.Идентификатор = "unique-token-id"; + Токен.КлючСопоставленияПользователя = "john.doe@example.com"; + Токен.Эмитент = "https://auth.example.com"; + + Токен.Заголовки["typ"] = "JWT"; + + Токен.ПолезнаяНагрузка["role"] = "admin"; + Токен.ПолезнаяНагрузка["permissions"] = "read,write,delete"; + + Токен.Получатели.Добавить("api.example.com"); + Токен.Получатели.Добавить("mobile.app"); + + РазобранныйТокен = РазобратьJWT(Токен); + + юТест.ПроверитьРавенство(РазобранныйТокен.Заголовки, "{""alg"":""none"",""typ"":""JWT""}", "Заголовки"); + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, "{""iss"":""https://auth.example.com"",""iat"":1640995200,""nbf"":1640995200,""exp"":1640998800,""jti"":""unique-token-id"",""sub"":""john.doe@example.com"",""aud"":[""api.example.com"",""mobile.app""],""role"":""admin"",""permissions"":""read,write,delete""}", "Полезная нагрузка"); КонецПроцедуры @@ -153,7 +185,7 @@ Токен.ПолезнаяНагрузка.Вставить("Соответствие", Соответствие); РазобранныйТокен = РазобратьJWT(Токен); - + юТест.ПроверитьРавенство(РазобранныйТокен.ПолезнаяНагрузка, ОжидаемоеЗначение); КонецПроцедуры @@ -227,6 +259,7 @@ КонецЕсли; Возврат Значение + Паддинг; + КонецФункции // 2048 bits, PKCS#8 From 29f9a34ee1ca15ad08e5301f210b377207b9f5e1 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 9 Dec 2025 00:10:21 +0300 Subject: [PATCH 6/7] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=20=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/http.os | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/tests/http.os b/tests/http.os index 5849048d6..f74278bab 100644 --- a/tests/http.os +++ b/tests/http.os @@ -1,6 +1,7 @@ Перем юТест; Перем мАдресРесурса; // URL ресурса (хоста) httpbin.org для тестирования запросов +Перем мПриватныйКлючRSA; // 2048 bits, PKCS#8 Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт @@ -373,17 +374,22 @@ Процедура ТестДолженПроверитьПередачуТокенаЧерезДобавитьТокенДоступа() Экспорт - Токен = Новый ТокенДоступа(); + ТокенДоступа = Новый ТокенДоступа(); + ТокенДоступа.Эмитент = "ERP"; + ТокенДоступа.Получатели.Добавить("ДО"); + ТокенДоступа.ВремяСоздания = ТекущаяУниверсальнаяДата() - Дата(1970, 1, 1, 0, 0, 0); + ТокенДоступа.ВремяЖизни = 3600; + ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, мПриватныйКлючRSA); Запрос = Новый HttpЗапрос("/get"); - Запрос.ДобавитьТокенДоступа(Токен); + Запрос.ДобавитьТокенДоступа(ТокенДоступа); Соединение = Новый HttpСоединение(мАдресРесурса); Ответ = Соединение.ВызватьHTTPМетод("GET", Запрос); ТелоОтвета = Ответ.ПолучитьТелоКакСтроку(); юТест.ПроверитьРавенство(200, Ответ.КодСостояния); - юТест.ПроверитьВхождение(JsonВОбъект(ТелоОтвета)["headers"]["Authorization"], "Bearer " + Токен); + юТест.ПроверитьВхождение(JsonВОбъект(ТелоОтвета)["headers"]["Authorization"], "Bearer " + ТокенДоступа); КонецПроцедуры @@ -404,3 +410,32 @@ /////////////////////////////////////////////////////////////////// мАдресРесурса = "httpbin.org"; +мПриватныйКлючRSA = " + |-----BEGIN PRIVATE KEY----- + |MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT4Ws/kRkM92gs + |rljP85+ZaBoE/aJa5yBoWOJI5notYGDwAU66WofBunLIlUNn/YuETvs12mCCClBX + |ODurlwi+wLs5HyXf1gqNzVP4JN5QFUpqd2RQrUZLqBoONjZAok9P3oAw4JuJDnBm + |JODFLQjbCUOvorxR33iCPDfflvSa8kIxkQ3hA6zB7vu0j5s8HGCXczRI0pSblAH3 + |GEPSPp8D4IBmVKi90lQjCVVlCMY/hVkecqjc2hhfsh2cF/R2Q3NptNSLtw+lEMOU + |uUQtgVnULhTYDRaRJ9HripxQvQ2Se/BnozFVPfKokouh6f0ti9b4vWSDN3EjuMEu + |wf+ttUPhAgMBAAECggEAZRT3lzraMbfHmIsQIAz6MgUri+/HE3Xa/BiNjKEmMHAp + |ssRiCRWqqCyHGz6UFbgErsrCAhykTR85rXS9rNuPWbwp/bCh1e/bxPPuCrdA0uLK + |vB2iD1dxrnNYZBCAYwvPRNvlKgPNrRCWmetpTL4syHP7tUl6ikhDelopIwdvaZ1W + |cQeDq6QqkzOcJFdXHIHxZ2qbnjJoinyfPzmA98Ijdi8ynjIqKxj0f+0r567nxe/q + |Z4FwbPi7bRSmfMsTCM0SagwMuQoqvB6iktrZEYYUJmcN7V/gjN1viqVkjudk/CC5 + |RgMBp7yhrt3SP2oErAaAlTWldRVJ8qquJSpXkBhTxQKBgQDM8S9WIZgl2HwWsK9c + |C2JTC0peB8W1B3rM5IYvrCN0uB/P/KZ/Igwkla3wsOhy2SDPBYqzP/4OPzz4atOL + |PFDdBGrIQgseSajPp7nuD91IjI9KFXUT1EUBbKhWmATZwQqCOlHtSoi4zl+dJWgr + |dVzQxykh3ME85mIU3cxWNxk+swKBgQC4uPSsE9aJ5JK5LCfpcsVYeeatko72rA2t + |N/aPActklWwkR/tvJ43jG5RwKwbY4N2V6a5Tz5xQIe550vvBd56ku6tmEJDLL+jc + |QLh5lajV3JJq+tazdyZkztaSwOucuZve9R+eYAPM3uqYO8gAJSrJQQwlAoJkrWk6 + |zoISqvk9GwKBgFJQsVWkCqtwx26JqvWKcQSv7T/VWVi711wCkc8GEfuolMaCURGR + |SFVNdV7Of57ZjS75p9sVYeKxDbktlyg+orATPRyQQkz5Av/c+3YeEyA3rLnx4mOD + |h+/ph7e4OYKI4cKq0AtCO6YW5hqFUtDZw9zrkZ7TPx5J3q1I5PDgEpi3AoGAYokO + |wwfmBjJubeZ299loGdUUzvwB1OVcek0C8a/kXigywnu/TMDNuBrLKLZa9a+lo8U+ + |X6i8WiZvfn6kGsSDVJ5jRJOLmUdaMLs5bGn/4OzDEdvyUVM6oMWQwkG0rSp5Ugpx + |rlrLTCqQ+1nQSvuIgkh3gPqAsdGq81qbGfgn8YcCgYEAjodWERs0eCSMNGIshss4 + |/GN50lbOiI0+ke99160kT/hpep1WCEpXBfCeXLBamcG36Uk8c34DrxJ2Uj8LC895 + |/tB4HFgbQ52VxQdQCNGcs9hOxPuLlXlSdKNuplvfAkg2JhMrES7KtAxOBAcK77kx + |JzEREQEFN8CmwDvSpw8Mz9k= + |-----END PRIVATE KEY-----"; \ No newline at end of file From ea2fd0eb47031b9e54bf8005e9e13faf7872c689 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 9 Dec 2025 00:47:36 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D0=B0=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B8=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/http.os | 41 ++++------------------- tests/{ => security}/access-token.os | 49 ++++------------------------ tests/security/test_ecdsa_key.pem | 5 +++ tests/security/test_rsa_key.pem | 28 ++++++++++++++++ 4 files changed, 45 insertions(+), 78 deletions(-) rename tests/{ => security}/access-token.os (88%) create mode 100644 tests/security/test_ecdsa_key.pem create mode 100644 tests/security/test_rsa_key.pem diff --git a/tests/http.os b/tests/http.os index 079e6463c..da6f3f5a0 100644 --- a/tests/http.os +++ b/tests/http.os @@ -1,7 +1,6 @@ Перем юТест; Перем мАдресРесурса; // URL ресурса (хоста) для тестирования запросов -Перем мПриватныйКлючRSA; // 2048 bits, PKCS#8 Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт @@ -386,7 +385,7 @@ ТокенДоступа.Получатели.Добавить("ДО"); ТокенДоступа.ВремяСоздания = ТекущаяУниверсальнаяДата() - Дата(1970, 1, 1, 0, 0, 0); ТокенДоступа.ВремяЖизни = 3600; - ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, мПриватныйКлючRSA); + ТокенДоступа.Подписать(АлгоритмПодписиТокенаДоступа.HS256, ПрочитатьПриватныйКлючRSA()); Запрос = Новый HttpЗапрос("/get"); Запрос.ДобавитьТокенДоступа(ТокенДоступа); @@ -458,36 +457,8 @@ КонецФункции -/////////////////////////////////////////////////////////////////// -/// ИНИЦИАЛИЗАЦИЯ -/////////////////////////////////////////////////////////////////// - -мПриватныйКлючRSA = " - |-----BEGIN PRIVATE KEY----- - |MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT4Ws/kRkM92gs - |rljP85+ZaBoE/aJa5yBoWOJI5notYGDwAU66WofBunLIlUNn/YuETvs12mCCClBX - |ODurlwi+wLs5HyXf1gqNzVP4JN5QFUpqd2RQrUZLqBoONjZAok9P3oAw4JuJDnBm - |JODFLQjbCUOvorxR33iCPDfflvSa8kIxkQ3hA6zB7vu0j5s8HGCXczRI0pSblAH3 - |GEPSPp8D4IBmVKi90lQjCVVlCMY/hVkecqjc2hhfsh2cF/R2Q3NptNSLtw+lEMOU - |uUQtgVnULhTYDRaRJ9HripxQvQ2Se/BnozFVPfKokouh6f0ti9b4vWSDN3EjuMEu - |wf+ttUPhAgMBAAECggEAZRT3lzraMbfHmIsQIAz6MgUri+/HE3Xa/BiNjKEmMHAp - |ssRiCRWqqCyHGz6UFbgErsrCAhykTR85rXS9rNuPWbwp/bCh1e/bxPPuCrdA0uLK - |vB2iD1dxrnNYZBCAYwvPRNvlKgPNrRCWmetpTL4syHP7tUl6ikhDelopIwdvaZ1W - |cQeDq6QqkzOcJFdXHIHxZ2qbnjJoinyfPzmA98Ijdi8ynjIqKxj0f+0r567nxe/q - |Z4FwbPi7bRSmfMsTCM0SagwMuQoqvB6iktrZEYYUJmcN7V/gjN1viqVkjudk/CC5 - |RgMBp7yhrt3SP2oErAaAlTWldRVJ8qquJSpXkBhTxQKBgQDM8S9WIZgl2HwWsK9c - |C2JTC0peB8W1B3rM5IYvrCN0uB/P/KZ/Igwkla3wsOhy2SDPBYqzP/4OPzz4atOL - |PFDdBGrIQgseSajPp7nuD91IjI9KFXUT1EUBbKhWmATZwQqCOlHtSoi4zl+dJWgr - |dVzQxykh3ME85mIU3cxWNxk+swKBgQC4uPSsE9aJ5JK5LCfpcsVYeeatko72rA2t - |N/aPActklWwkR/tvJ43jG5RwKwbY4N2V6a5Tz5xQIe550vvBd56ku6tmEJDLL+jc - |QLh5lajV3JJq+tazdyZkztaSwOucuZve9R+eYAPM3uqYO8gAJSrJQQwlAoJkrWk6 - |zoISqvk9GwKBgFJQsVWkCqtwx26JqvWKcQSv7T/VWVi711wCkc8GEfuolMaCURGR - |SFVNdV7Of57ZjS75p9sVYeKxDbktlyg+orATPRyQQkz5Av/c+3YeEyA3rLnx4mOD - |h+/ph7e4OYKI4cKq0AtCO6YW5hqFUtDZw9zrkZ7TPx5J3q1I5PDgEpi3AoGAYokO - |wwfmBjJubeZ299loGdUUzvwB1OVcek0C8a/kXigywnu/TMDNuBrLKLZa9a+lo8U+ - |X6i8WiZvfn6kGsSDVJ5jRJOLmUdaMLs5bGn/4OzDEdvyUVM6oMWQwkG0rSp5Ugpx - |rlrLTCqQ+1nQSvuIgkh3gPqAsdGq81qbGfgn8YcCgYEAjodWERs0eCSMNGIshss4 - |/GN50lbOiI0+ke99160kT/hpep1WCEpXBfCeXLBamcG36Uk8c34DrxJ2Uj8LC895 - |/tB4HFgbQ52VxQdQCNGcs9hOxPuLlXlSdKNuplvfAkg2JhMrES7KtAxOBAcK77kx - |JzEREQEFN8CmwDvSpw8Mz9k= - |-----END PRIVATE KEY-----"; \ No newline at end of file +Функция ПрочитатьПриватныйКлючRSA() + ПутьКФайлу = ОбъединитьПути(ТекущийСценарий().Каталог, "./security/test_rsa_key.pem"); + ЧтениеТекста = Новый ЧтениеТекста(ПутьКФайлу); + Возврат ЧтениеТекста.Прочитать(); +КонецФункции \ No newline at end of file diff --git a/tests/access-token.os b/tests/security/access-token.os similarity index 88% rename from tests/access-token.os rename to tests/security/access-token.os index c411ba5af..ddd9bffb9 100644 --- a/tests/access-token.os +++ b/tests/security/access-token.os @@ -87,8 +87,8 @@ Процедура ТестДолжен_ПроверитьГенерациюТокенаСКлючом() Экспорт - ПриватныйКлючECDSA = ПриватныйКлючECDSA(); - ПриватныйКлючRSA = ПриватныйКлючRSA(); + ПриватныйКлючRSA = ПрочитатьТекстовыйФайл("./test_rsa_key.pem"); + ПриватныйКлючECDSA = ПрочитатьТекстовыйФайл("./test_ecdsa_key.pem"); ТестовыеСлучаи = Новый Соответствие(); ТестовыеСлучаи.Вставить(АлгоритмПодписиТокенаДоступа.ES256, ПриватныйКлючECDSA); @@ -262,45 +262,8 @@ КонецФункции -// 2048 bits, PKCS#8 -Функция ПриватныйКлючRSA() Экспорт - Возврат " - |-----BEGIN PRIVATE KEY----- - |MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT4Ws/kRkM92gs - |rljP85+ZaBoE/aJa5yBoWOJI5notYGDwAU66WofBunLIlUNn/YuETvs12mCCClBX - |ODurlwi+wLs5HyXf1gqNzVP4JN5QFUpqd2RQrUZLqBoONjZAok9P3oAw4JuJDnBm - |JODFLQjbCUOvorxR33iCPDfflvSa8kIxkQ3hA6zB7vu0j5s8HGCXczRI0pSblAH3 - |GEPSPp8D4IBmVKi90lQjCVVlCMY/hVkecqjc2hhfsh2cF/R2Q3NptNSLtw+lEMOU - |uUQtgVnULhTYDRaRJ9HripxQvQ2Se/BnozFVPfKokouh6f0ti9b4vWSDN3EjuMEu - |wf+ttUPhAgMBAAECggEAZRT3lzraMbfHmIsQIAz6MgUri+/HE3Xa/BiNjKEmMHAp - |ssRiCRWqqCyHGz6UFbgErsrCAhykTR85rXS9rNuPWbwp/bCh1e/bxPPuCrdA0uLK - |vB2iD1dxrnNYZBCAYwvPRNvlKgPNrRCWmetpTL4syHP7tUl6ikhDelopIwdvaZ1W - |cQeDq6QqkzOcJFdXHIHxZ2qbnjJoinyfPzmA98Ijdi8ynjIqKxj0f+0r567nxe/q - |Z4FwbPi7bRSmfMsTCM0SagwMuQoqvB6iktrZEYYUJmcN7V/gjN1viqVkjudk/CC5 - |RgMBp7yhrt3SP2oErAaAlTWldRVJ8qquJSpXkBhTxQKBgQDM8S9WIZgl2HwWsK9c - |C2JTC0peB8W1B3rM5IYvrCN0uB/P/KZ/Igwkla3wsOhy2SDPBYqzP/4OPzz4atOL - |PFDdBGrIQgseSajPp7nuD91IjI9KFXUT1EUBbKhWmATZwQqCOlHtSoi4zl+dJWgr - |dVzQxykh3ME85mIU3cxWNxk+swKBgQC4uPSsE9aJ5JK5LCfpcsVYeeatko72rA2t - |N/aPActklWwkR/tvJ43jG5RwKwbY4N2V6a5Tz5xQIe550vvBd56ku6tmEJDLL+jc - |QLh5lajV3JJq+tazdyZkztaSwOucuZve9R+eYAPM3uqYO8gAJSrJQQwlAoJkrWk6 - |zoISqvk9GwKBgFJQsVWkCqtwx26JqvWKcQSv7T/VWVi711wCkc8GEfuolMaCURGR - |SFVNdV7Of57ZjS75p9sVYeKxDbktlyg+orATPRyQQkz5Av/c+3YeEyA3rLnx4mOD - |h+/ph7e4OYKI4cKq0AtCO6YW5hqFUtDZw9zrkZ7TPx5J3q1I5PDgEpi3AoGAYokO - |wwfmBjJubeZ299loGdUUzvwB1OVcek0C8a/kXigywnu/TMDNuBrLKLZa9a+lo8U+ - |X6i8WiZvfn6kGsSDVJ5jRJOLmUdaMLs5bGn/4OzDEdvyUVM6oMWQwkG0rSp5Ugpx - |rlrLTCqQ+1nQSvuIgkh3gPqAsdGq81qbGfgn8YcCgYEAjodWERs0eCSMNGIshss4 - |/GN50lbOiI0+ke99160kT/hpep1WCEpXBfCeXLBamcG36Uk8c34DrxJ2Uj8LC895 - |/tB4HFgbQ52VxQdQCNGcs9hOxPuLlXlSdKNuplvfAkg2JhMrES7KtAxOBAcK77kx - |JzEREQEFN8CmwDvSpw8Mz9k= - |-----END PRIVATE KEY-----"; -КонецФункции - -// secp256r1 -Функция ПриватныйКлючECDSA() - Возврат " - |-----BEGIN PRIVATE KEY----- - |MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZYdbovRCqelX1FPY - |EE8VAbDJ/cT99z8r8ZisRgftCt6hRANCAASFDFrXFeOV0rYTDuIDJHKUdwqc7Z3z - |gCxaeTCRJ+KBCw7AsGearOWLn67Y424YAx73wr3T9BIrQ3yGgPXZmzUR - |-----END PRIVATE KEY-----"; +Функция ПрочитатьТекстовыйФайл(ОтносительныйПуть) + ПутьКФайлу = ОбъединитьПути(ТекущийСценарий().Каталог, ОтносительныйПуть); + ЧтениеТекста = Новый ЧтениеТекста(ПутьКФайлу); + Возврат ЧтениеТекста.Прочитать(); КонецФункции \ No newline at end of file diff --git a/tests/security/test_ecdsa_key.pem b/tests/security/test_ecdsa_key.pem new file mode 100644 index 000000000..b406ad462 --- /dev/null +++ b/tests/security/test_ecdsa_key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgZYdbovRCqelX1FPY +EE8VAbDJ/cT99z8r8ZisRgftCt6hRANCAASFDFrXFeOV0rYTDuIDJHKUdwqc7Z3z +gCxaeTCRJ+KBCw7AsGearOWLn67Y424YAx73wr3T9BIrQ3yGgPXZmzUR +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/tests/security/test_rsa_key.pem b/tests/security/test_rsa_key.pem new file mode 100644 index 000000000..fda2e75f1 --- /dev/null +++ b/tests/security/test_rsa_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT4Ws/kRkM92gs +rljP85+ZaBoE/aJa5yBoWOJI5notYGDwAU66WofBunLIlUNn/YuETvs12mCCClBX +ODurlwi+wLs5HyXf1gqNzVP4JN5QFUpqd2RQrUZLqBoONjZAok9P3oAw4JuJDnBm +JODFLQjbCUOvorxR33iCPDfflvSa8kIxkQ3hA6zB7vu0j5s8HGCXczRI0pSblAH3 +GEPSPp8D4IBmVKi90lQjCVVlCMY/hVkecqjc2hhfsh2cF/R2Q3NptNSLtw+lEMOU +uUQtgVnULhTYDRaRJ9HripxQvQ2Se/BnozFVPfKokouh6f0ti9b4vWSDN3EjuMEu +wf+ttUPhAgMBAAECggEAZRT3lzraMbfHmIsQIAz6MgUri+/HE3Xa/BiNjKEmMHAp +ssRiCRWqqCyHGz6UFbgErsrCAhykTR85rXS9rNuPWbwp/bCh1e/bxPPuCrdA0uLK +vB2iD1dxrnNYZBCAYwvPRNvlKgPNrRCWmetpTL4syHP7tUl6ikhDelopIwdvaZ1W +cQeDq6QqkzOcJFdXHIHxZ2qbnjJoinyfPzmA98Ijdi8ynjIqKxj0f+0r567nxe/q +Z4FwbPi7bRSmfMsTCM0SagwMuQoqvB6iktrZEYYUJmcN7V/gjN1viqVkjudk/CC5 +RgMBp7yhrt3SP2oErAaAlTWldRVJ8qquJSpXkBhTxQKBgQDM8S9WIZgl2HwWsK9c +C2JTC0peB8W1B3rM5IYvrCN0uB/P/KZ/Igwkla3wsOhy2SDPBYqzP/4OPzz4atOL +PFDdBGrIQgseSajPp7nuD91IjI9KFXUT1EUBbKhWmATZwQqCOlHtSoi4zl+dJWgr +dVzQxykh3ME85mIU3cxWNxk+swKBgQC4uPSsE9aJ5JK5LCfpcsVYeeatko72rA2t +N/aPActklWwkR/tvJ43jG5RwKwbY4N2V6a5Tz5xQIe550vvBd56ku6tmEJDLL+jc +QLh5lajV3JJq+tazdyZkztaSwOucuZve9R+eYAPM3uqYO8gAJSrJQQwlAoJkrWk6 +zoISqvk9GwKBgFJQsVWkCqtwx26JqvWKcQSv7T/VWVi711wCkc8GEfuolMaCURGR +SFVNdV7Of57ZjS75p9sVYeKxDbktlyg+orATPRyQQkz5Av/c+3YeEyA3rLnx4mOD +h+/ph7e4OYKI4cKq0AtCO6YW5hqFUtDZw9zrkZ7TPx5J3q1I5PDgEpi3AoGAYokO +wwfmBjJubeZ299loGdUUzvwB1OVcek0C8a/kXigywnu/TMDNuBrLKLZa9a+lo8U+ +X6i8WiZvfn6kGsSDVJ5jRJOLmUdaMLs5bGn/4OzDEdvyUVM6oMWQwkG0rSp5Ugpx +rlrLTCqQ+1nQSvuIgkh3gPqAsdGq81qbGfgn8YcCgYEAjodWERs0eCSMNGIshss4 +/GN50lbOiI0+ke99160kT/hpep1WCEpXBfCeXLBamcG36Uk8c34DrxJ2Uj8LC895 +/tB4HFgbQ52VxQdQCNGcs9hOxPuLlXlSdKNuplvfAkg2JhMrES7KtAxOBAcK77kx +JzEREQEFN8CmwDvSpw8Mz9k= +-----END PRIVATE KEY----- \ No newline at end of file