From c427006283c824d21b29f9d2b56f796eb87edc07 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 03:52:31 +0300 Subject: [PATCH 01/45] docs: add env data --- .env | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/.env b/.env index af67fc3..f06ff63 100644 --- a/.env +++ b/.env @@ -16,4 +16,86 @@ # databases (if Sqlite) and other data that should be saved in a place other than the place where the program is launched. # REQUIRED if the application is inside the container # If you want to change this value, you need to change the values in Settings.json and move the file itself to the desired location. -PATH_TO_SAVE= \ No newline at end of file +PATH_TO_SAVE= + +# Security + +# JWT signature token +# string (UTF8) +# This token will be used to create and verify the signature of JWT tokens. +# The token must be equal to 64 characters +SECURITY_SIGNING_TOKEN= + +# Token for JWT encryption +# string (UTF8) +# This token will be used to encrypt and decrypt JWT tokens. +# The token must be equal to 32 characters +SECURITY_ENCRYPTION_TOKEN= + +# Time in minutes, which indicates after which time the Refresh Token will become invalid +# integer +# The token indicates how long after the user is inactive, he will need to log in again +SECURITY_LIFE_TIME_RT=1440 + +# The time in a minute, which indicates that this is exactly what it takes to become a non-state +# integer +# Do not specify a time that is too long or too short. Optimally 5 > x > 60 +SECURITY_LIFE_TIME_JWT=15 + +# Time in minutes, which indicates after which time the token of the first factor will become invalid +# integer +# Do not specify a short time. The user must be able to log in using the second factor +SECURITY_LIFE_TIME_1_FA=15 + +# An identifier that points to the server that created the token +# string +SECURITY_JWT_ISSUER= + +# ID of the audience for which the token is intended +# string +SECURITY_JWT_AUDIENCE= + +### Hashing + +# In order to set up hashing correctly, you need to start from the security requirements +# You can use the settings that were used in https://github.com/P-H-C/phc-winner-argon2 +# These parameters have a STRONG impact on performance +# When testing the system, these values were used: +# 10 <= SECURITY_HASH_ITERATION <= 25 iterations +# 16384 <= SECURITY_HASH_MEMORY <= 32768 KB +# 4 <= SECURITY_HASH_PARALLELISM <= 8 lines +# If we take all the large values, it will take a little more than 1 second to get the hash. If this time is critical, reduce the parameters + +# The number of iterations used to hash passwords in the Argon2 algorithm +# integer +# This parameter determines the number of iterations that the Argon2 algorithm goes through when hashing passwords. +# Increasing this value can improve security by increasing the time it takes to calculate the password hash. +# The average number of iterations to increase the security level should be set to at least 10. +SECURITY_HASH_ITERATION= + +# The amount of memory used to hash passwords in the Argon2 algorithm +# integer +# 65536 +# This parameter determines the number of kilobytes of memory that will be used for the password hashing process. +# Increasing this value may increase security, but it may also require more system resources. +SECURITY_HASH_MEMORY= + +# Parallelism determines how many of the memory fragments divided into strips will be used to generate a hash +# integer +# This value affects the hash itself, but can be changed to achieve an ideal execution time, taking into account the processor and the number of cores. +SECURITY_HASH_PARALLELISM= + +# The size of the output hash generated by the password hashing algorithm +# integer +SECURITY_HASH_SIZE=32 + +# Additional protection for Argon2 +# string (BASE64) +# (optional) +# We recommend installing a token so that even if the data is compromised, an attacker cannot brute force a password without a token +SECURITY_HASH_TOKEN= + +# The size of the salt used to hash passwords +# integer +# The salt is a random value added to the password before hashing to prevent the use of rainbow hash tables and other attacks. +SECURITY_SALT_SIZE=16 \ No newline at end of file From 7e283fe6437f90eb9b2529541399884d93fc6047 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:03:20 +0300 Subject: [PATCH 02/45] feat: add security layer --- Security/Security.csproj | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Security/Security.csproj diff --git a/Security/Security.csproj b/Security/Security.csproj new file mode 100644 index 0000000..94ec65a --- /dev/null +++ b/Security/Security.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + disable + enable + Winsomnia + 1.0.0-a0 + 1.0.0.0 + 1.0.0.0 + Mirea.Api.Security + $(AssemblyName) + + + From 930edd4c2c83f36185d3b165c7fa9c35a68f2687 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:03:47 +0300 Subject: [PATCH 03/45] build: add ref --- Security/Security.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Security/Security.csproj b/Security/Security.csproj index 94ec65a..d9e1953 100644 --- a/Security/Security.csproj +++ b/Security/Security.csproj @@ -12,4 +12,8 @@ $(AssemblyName) + + + + From e1123cf36b98515a841199b7a5b18b7e7f91e1a4 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:04:02 +0300 Subject: [PATCH 04/45] feat: add password hashing --- Security/PasswordHashService.cs | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 Security/PasswordHashService.cs diff --git a/Security/PasswordHashService.cs b/Security/PasswordHashService.cs new file mode 100644 index 0000000..a299901 --- /dev/null +++ b/Security/PasswordHashService.cs @@ -0,0 +1,76 @@ +using System.Buffers.Text; +using System.Text; +using Konscious.Security.Cryptography; + +namespace Security; + +public class PasswordHashService +{ + public int SaltSize { private get; init; } + public int HashSize { private get; init; } + public int Iterations { private get; init; } + public int Memory { private get; init; } + public int Parallelism { private get; init; } + public string? Secret { private get; init; } + + private ReadOnlySpan HashPassword(string password, ReadOnlySpan salt) + { + var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) + { + Iterations = Iterations, + MemorySize = Memory, + DegreeOfParallelism = Parallelism, + Salt = salt.ToArray() + }; + + if (!string.IsNullOrEmpty(Secret)) + argon2.KnownSecret = Convert.FromBase64String(Secret); + + return argon2.GetBytes(HashSize); + } + + private static bool ConstantTimeComparison(ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length) + return false; + + int result = 0; + for (int i = 0; i < a.Length; i++) + result |= a[i] ^ b[i]; + return result == 0; + } + + public static ReadOnlySpan GenerateRandomKeyBytes(int size) + { + var key = new byte[size]; + using var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(key); + return key; + } + + public static string GenerateRandomKeyStringBase64(int size) => + Convert.ToBase64String(GenerateRandomKeyBytes(size)); + + public static string GenerateRandomKeyString(int size) + { + var randomBytes = GenerateRandomKeyBytes(size); + Span utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)]; + + Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _); + return Encoding.UTF8.GetString(utf8Bytes); + } + + public (string Salt, string Hash) HashPassword(string password) + { + var salt = GenerateRandomKeyBytes(SaltSize); + var hash = HashPassword(password, salt); + + return (Convert.ToBase64String(salt), Convert.ToBase64String(hash)); + } + + public bool VerifyPassword(string password, ReadOnlySpan salt, ReadOnlySpan hash) => + ConstantTimeComparison(HashPassword(password, salt), hash); + + public bool VerifyPassword(string password, string saltBase64, string hashBase64) => + VerifyPassword(password, Convert.FromBase64String(saltBase64), Convert.FromBase64String(hashBase64)); +} \ No newline at end of file From 3149f50586cb54dde41731623f6e1dcb7c4ff72d Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:05:18 +0300 Subject: [PATCH 05/45] refactor: move class to correct namespace --- Security/PasswordHashService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Security/PasswordHashService.cs b/Security/PasswordHashService.cs index a299901..9ea24b3 100644 --- a/Security/PasswordHashService.cs +++ b/Security/PasswordHashService.cs @@ -1,8 +1,9 @@ -using System.Buffers.Text; +using Konscious.Security.Cryptography; +using System; +using System.Buffers.Text; using System.Text; -using Konscious.Security.Cryptography; -namespace Security; +namespace Mirea.Api.Security; public class PasswordHashService { From e3dd0a84190cf3414e5639d912f78a8e793952e8 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:08:51 +0300 Subject: [PATCH 06/45] build: add ref for DI --- Security/Security.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Security/Security.csproj b/Security/Security.csproj index d9e1953..218d9f6 100644 --- a/Security/Security.csproj +++ b/Security/Security.csproj @@ -14,6 +14,8 @@ + + From 656d7dca0baa92246884fe31f4b27d1e6bffb482 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:09:10 +0300 Subject: [PATCH 07/45] feat: add DI --- Security/DependencyInjection.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Security/DependencyInjection.cs diff --git a/Security/DependencyInjection.cs b/Security/DependencyInjection.cs new file mode 100644 index 0000000..4dbab98 --- /dev/null +++ b/Security/DependencyInjection.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Mirea.Api.Security; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration) + { + var saltSize = int.Parse(configuration["SECURITY_SALT_SIZE"]!); + var hashSize = int.Parse(configuration["SECURITY_HASH_SIZE"]!); + var iteration = int.Parse(configuration["SECURITY_HASH_ITERATION"]!); + var memory = int.Parse(configuration["SECURITY_HASH_MEMORY"]!); + var parallelism = int.Parse(configuration["SECURITY_HASH_PARALLELISM"]!); + + services.AddSingleton(new PasswordHashService + { + SaltSize = saltSize, + HashSize = hashSize, + Iterations = iteration, + Memory = memory, + Parallelism = parallelism, + Secret = configuration["SECURITY_HASH_TOKEN"] + }); + + return services; + } +} \ No newline at end of file From 6029ea3c2cc20ebfa1958ce5524d5db83b82e25a Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:11:04 +0300 Subject: [PATCH 08/45] refactor: move hashing to services --- Security/DependencyInjection.cs | 3 ++- Security/{ => Services}/PasswordHashService.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) rename Security/{ => Services}/PasswordHashService.cs (98%) diff --git a/Security/DependencyInjection.cs b/Security/DependencyInjection.cs index 4dbab98..39e113a 100644 --- a/Security/DependencyInjection.cs +++ b/Security/DependencyInjection.cs @@ -1,11 +1,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Mirea.Api.Security.Services; namespace Mirea.Api.Security; public static class DependencyInjection { - public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddSecurityServices(this IServiceCollection services, IConfiguration configuration) { var saltSize = int.Parse(configuration["SECURITY_SALT_SIZE"]!); var hashSize = int.Parse(configuration["SECURITY_HASH_SIZE"]!); diff --git a/Security/PasswordHashService.cs b/Security/Services/PasswordHashService.cs similarity index 98% rename from Security/PasswordHashService.cs rename to Security/Services/PasswordHashService.cs index 9ea24b3..b9cc41c 100644 --- a/Security/PasswordHashService.cs +++ b/Security/Services/PasswordHashService.cs @@ -3,7 +3,7 @@ using System; using System.Buffers.Text; using System.Text; -namespace Mirea.Api.Security; +namespace Mirea.Api.Security.Services; public class PasswordHashService { From f749ed42f5ba34458a238db9befbc7a66ec4545e Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:29:50 +0300 Subject: [PATCH 09/45] feat: add interface for save to cache --- Security/Common/Interfaces/ICacheService.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Security/Common/Interfaces/ICacheService.cs diff --git a/Security/Common/Interfaces/ICacheService.cs b/Security/Common/Interfaces/ICacheService.cs new file mode 100644 index 0000000..c2a419b --- /dev/null +++ b/Security/Common/Interfaces/ICacheService.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Mirea.Api.Security.Common.Interfaces; + +public interface ICacheService +{ + Task SetAsync(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, CancellationToken cancellationToken = default); + Task GetAsync(string key, CancellationToken cancellationToken = default); + Task RemoveAsync(string key, CancellationToken cancellationToken = default); +} From 58ceca5313fa714f0418be3142c697269a7691b7 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:30:32 +0300 Subject: [PATCH 10/45] feat: add pre-auth token structure --- Security/Common/Domain/PreAuthToken.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Security/Common/Domain/PreAuthToken.cs diff --git a/Security/Common/Domain/PreAuthToken.cs b/Security/Common/Domain/PreAuthToken.cs new file mode 100644 index 0000000..4da9f6d --- /dev/null +++ b/Security/Common/Domain/PreAuthToken.cs @@ -0,0 +1,9 @@ +namespace Mirea.Api.Security.Common.Domain; + +public class PreAuthToken +{ + public required string Fingerprint { get; set; } + public required string UserAgent { get; set; } + public required string UserId { get; set; } + public required string Token { get; set; } +} \ No newline at end of file From e3db6b73e01fb86d983074190b3b27111beddf7c Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:30:55 +0300 Subject: [PATCH 11/45] feat: add pre-auth response --- Security/Common/Dto/Responses/PreAuthTokenResponse.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Security/Common/Dto/Responses/PreAuthTokenResponse.cs diff --git a/Security/Common/Dto/Responses/PreAuthTokenResponse.cs b/Security/Common/Dto/Responses/PreAuthTokenResponse.cs new file mode 100644 index 0000000..9a7238f --- /dev/null +++ b/Security/Common/Dto/Responses/PreAuthTokenResponse.cs @@ -0,0 +1,9 @@ +using System; + +namespace Mirea.Api.Security.Common.Dto.Responses; + +public class PreAuthTokenResponse +{ + public required string Token { get; set; } + public DateTime ExpiresIn { get; set; } +} \ No newline at end of file From 3c9694de08a9870453371f645f7616c6e8af0805 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:31:19 +0300 Subject: [PATCH 12/45] feat: add request for get token --- Security/Common/Dto/Requests/TokenRequest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Security/Common/Dto/Requests/TokenRequest.cs diff --git a/Security/Common/Dto/Requests/TokenRequest.cs b/Security/Common/Dto/Requests/TokenRequest.cs new file mode 100644 index 0000000..8be8038 --- /dev/null +++ b/Security/Common/Dto/Requests/TokenRequest.cs @@ -0,0 +1,8 @@ +namespace Mirea.Api.Security.Common.Dto.Requests; + +public class TokenRequest +{ + public required string Fingerprint { get; set; } + public required string UserAgent { get; set; } + public required string Ip { get; set; } +} \ No newline at end of file From b14ae26a4832ec8f41f43df58655e39f6151843d Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:31:47 +0300 Subject: [PATCH 13/45] feat: add pre-auth service --- Security/Services/PreAuthService.cs | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Security/Services/PreAuthService.cs diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs new file mode 100644 index 0000000..2a1929c --- /dev/null +++ b/Security/Services/PreAuthService.cs @@ -0,0 +1,42 @@ +using Mirea.Api.Security.Common.Domain; +using Mirea.Api.Security.Common.Dto.Requests; +using Mirea.Api.Security.Common.Dto.Responses; +using Mirea.Api.Security.Common.Interfaces; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Mirea.Api.Security.Services; + +public class PreAuthService(ICacheService cache) +{ + public TimeSpan Lifetime { private get; init; } + + private static string GenerateFirstAuthToken() => Guid.NewGuid().ToString().Replace("-", ""); + + public async Task CreateLoginTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default) + { + var firstAuthToken = GenerateFirstAuthToken(); + + var loginStructure = new PreAuthToken + { + Fingerprint = request.Fingerprint, + UserId = userId, + UserAgent = request.UserAgent, + Token = firstAuthToken + }; + + await cache.SetAsync( + request.Fingerprint, + JsonSerializer.SerializeToUtf8Bytes(loginStructure), + Lifetime, + cancellation); + + return new PreAuthTokenResponse + { + Token = firstAuthToken, + ExpiresIn = DateTime.UtcNow.Add(Lifetime) + }; + } +} \ No newline at end of file From 8408b80c35258aceba37ae9662407779957566c3 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:34:00 +0300 Subject: [PATCH 14/45] feat: add pre-auth to DI --- Security/DependencyInjection.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Security/DependencyInjection.cs b/Security/DependencyInjection.cs index 39e113a..5441ebc 100644 --- a/Security/DependencyInjection.cs +++ b/Security/DependencyInjection.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Mirea.Api.Security.Common.Interfaces; using Mirea.Api.Security.Services; +using System; namespace Mirea.Api.Security; @@ -24,6 +26,18 @@ public static class DependencyInjection Secret = configuration["SECURITY_HASH_TOKEN"] }); + var lifeTimeLogin = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_1_FA"]!)); + + services.AddSingleton(provider => + { + var cache = provider.GetRequiredService(); + + return new PreAuthService(cache) + { + Lifetime = lifeTimeLogin + }; + }); + return services; } } \ No newline at end of file From 5fde5bd3967f0ce86a7490ab3321364add1feb2a Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:34:39 +0300 Subject: [PATCH 15/45] build: add security to sln --- Backend.sln | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Backend.sln b/Backend.sln index 95ebfb7..5790acf 100644 --- a/Backend.sln +++ b/Backend.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "Persistence\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Security", "Security\Security.csproj", "{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {0335FA36-E137-453F-853B-916674C168FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = Release|Any CPU + {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From d05ba5349fd44eeef83335584d28bd9c2a4a08df Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:48:37 +0300 Subject: [PATCH 16/45] refactor: isolate key generation --- Security/Services/GeneratorKey.cs | 28 ++++++++++++++++++++++++ Security/Services/PasswordHashService.cs | 23 +------------------ 2 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 Security/Services/GeneratorKey.cs diff --git a/Security/Services/GeneratorKey.cs b/Security/Services/GeneratorKey.cs new file mode 100644 index 0000000..79a0430 --- /dev/null +++ b/Security/Services/GeneratorKey.cs @@ -0,0 +1,28 @@ +using System; +using System.Buffers.Text; +using System.Text; + +namespace Mirea.Api.Security.Services; + +public static class GeneratorKey +{ + public static ReadOnlySpan GenerateBytes(int size) + { + var key = new byte[size]; + using var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(key); + return key; + } + + public static string GenerateBase64(int size) => + Convert.ToBase64String(GenerateBytes(size)); + + public static string GenerateString(int size) + { + var randomBytes = GenerateBytes(size); + Span utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)]; + + Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _); + return Encoding.UTF8.GetString(utf8Bytes); + } +} \ No newline at end of file diff --git a/Security/Services/PasswordHashService.cs b/Security/Services/PasswordHashService.cs index b9cc41c..8673222 100644 --- a/Security/Services/PasswordHashService.cs +++ b/Security/Services/PasswordHashService.cs @@ -1,6 +1,5 @@ using Konscious.Security.Cryptography; using System; -using System.Buffers.Text; using System.Text; namespace Mirea.Api.Security.Services; @@ -41,29 +40,9 @@ public class PasswordHashService return result == 0; } - public static ReadOnlySpan GenerateRandomKeyBytes(int size) - { - var key = new byte[size]; - using var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); - rng.GetNonZeroBytes(key); - return key; - } - - public static string GenerateRandomKeyStringBase64(int size) => - Convert.ToBase64String(GenerateRandomKeyBytes(size)); - - public static string GenerateRandomKeyString(int size) - { - var randomBytes = GenerateRandomKeyBytes(size); - Span utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)]; - - Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _); - return Encoding.UTF8.GetString(utf8Bytes); - } - public (string Salt, string Hash) HashPassword(string password) { - var salt = GenerateRandomKeyBytes(SaltSize); + var salt = GeneratorKey.GenerateBytes(SaltSize); var hash = HashPassword(password, salt); return (Convert.ToBase64String(salt), Convert.ToBase64String(hash)); From 47a57693f86d8f64329b35dc7eef992071d47c8d Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:55:34 +0300 Subject: [PATCH 17/45] sec: complicate the token --- Security/Services/PreAuthService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs index 2a1929c..21c97ff 100644 --- a/Security/Services/PreAuthService.cs +++ b/Security/Services/PreAuthService.cs @@ -13,7 +13,8 @@ public class PreAuthService(ICacheService cache) { public TimeSpan Lifetime { private get; init; } - private static string GenerateFirstAuthToken() => Guid.NewGuid().ToString().Replace("-", ""); + private static string GeneratePreAuthToken() => Guid.NewGuid().ToString().Replace("-", "") + + GeneratorKey.GenerateString(16); public async Task CreateLoginTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default) { From ac7bbde75e07774428ce8d37d382629ffd937765 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:57:44 +0300 Subject: [PATCH 18/45] fix: add key for save pre auth token --- Security/Services/PreAuthService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs index 21c97ff..484c5a1 100644 --- a/Security/Services/PreAuthService.cs +++ b/Security/Services/PreAuthService.cs @@ -16,7 +16,7 @@ public class PreAuthService(ICacheService cache) private static string GeneratePreAuthToken() => Guid.NewGuid().ToString().Replace("-", "") + GeneratorKey.GenerateString(16); - public async Task CreateLoginTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default) + private static string GetPreAuthCacheKey(string fingerprint) => $"{fingerprint}_pre_auth_token"; { var firstAuthToken = GenerateFirstAuthToken(); @@ -29,7 +29,7 @@ public class PreAuthService(ICacheService cache) }; await cache.SetAsync( - request.Fingerprint, + GetPreAuthCacheKey(request.Fingerprint), JsonSerializer.SerializeToUtf8Bytes(loginStructure), Lifetime, cancellation); From f4ad1518ef5cbae22c0bc4531c8a2742cdf1d40f Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 04:58:21 +0300 Subject: [PATCH 19/45] style: rename variables --- Security/Services/PreAuthService.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs index 484c5a1..cc189dd 100644 --- a/Security/Services/PreAuthService.cs +++ b/Security/Services/PreAuthService.cs @@ -17,26 +17,28 @@ public class PreAuthService(ICacheService cache) GeneratorKey.GenerateString(16); private static string GetPreAuthCacheKey(string fingerprint) => $"{fingerprint}_pre_auth_token"; - { - var firstAuthToken = GenerateFirstAuthToken(); - var loginStructure = new PreAuthToken + public async Task GeneratePreAuthTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default) + { + var preAuthToken = GeneratePreAuthToken(); + + var preAuthTokenStruct = new PreAuthToken { Fingerprint = request.Fingerprint, UserId = userId, UserAgent = request.UserAgent, - Token = firstAuthToken + Token = preAuthToken }; await cache.SetAsync( GetPreAuthCacheKey(request.Fingerprint), - JsonSerializer.SerializeToUtf8Bytes(loginStructure), + JsonSerializer.SerializeToUtf8Bytes(preAuthTokenStruct), Lifetime, cancellation); return new PreAuthTokenResponse { - Token = firstAuthToken, + Token = preAuthToken, ExpiresIn = DateTime.UtcNow.Add(Lifetime) }; } From 916b3795ed7fc62414e23d9657d76f102ea3bb1e Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:27:27 +0300 Subject: [PATCH 20/45] feat: add ip to struct --- Security/Common/Domain/PreAuthToken.cs | 1 + Security/Services/PreAuthService.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Security/Common/Domain/PreAuthToken.cs b/Security/Common/Domain/PreAuthToken.cs index 4da9f6d..f1f9684 100644 --- a/Security/Common/Domain/PreAuthToken.cs +++ b/Security/Common/Domain/PreAuthToken.cs @@ -5,5 +5,6 @@ public class PreAuthToken public required string Fingerprint { get; set; } public required string UserAgent { get; set; } public required string UserId { get; set; } + public required string Ip { get; set; } public required string Token { get; set; } } \ No newline at end of file diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs index cc189dd..2bb5390 100644 --- a/Security/Services/PreAuthService.cs +++ b/Security/Services/PreAuthService.cs @@ -27,7 +27,8 @@ public class PreAuthService(ICacheService cache) Fingerprint = request.Fingerprint, UserId = userId, UserAgent = request.UserAgent, - Token = preAuthToken + Token = preAuthToken, + Ip = request.Ip }; await cache.SetAsync( From 470031af39fb36c4068a36d87b2f46822243e27a Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:27:49 +0300 Subject: [PATCH 21/45] feat: add match token --- Security/Services/PreAuthService.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs index 2bb5390..1a0443e 100644 --- a/Security/Services/PreAuthService.cs +++ b/Security/Services/PreAuthService.cs @@ -3,6 +3,7 @@ using Mirea.Api.Security.Common.Dto.Requests; using Mirea.Api.Security.Common.Dto.Responses; using Mirea.Api.Security.Common.Interfaces; using System; +using System.Security; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -43,4 +44,21 @@ public class PreAuthService(ICacheService cache) ExpiresIn = DateTime.UtcNow.Add(Lifetime) }; } + public async Task MatchToken(TokenRequest request, string preAuthToken, CancellationToken cancellation = default) + { + var preAuthTokenStruct = await cache.GetAsync(GetPreAuthCacheKey(request.Fingerprint), cancellation) + ?? throw new SecurityException($"The token was not found using fingerprint \"{request.Fingerprint}\""); + + if (preAuthTokenStruct == null || + preAuthTokenStruct.Token != preAuthToken || + (preAuthTokenStruct.UserAgent != request.UserAgent && + preAuthTokenStruct.Ip != request.Ip)) + { + throw new SecurityException("It was not possible to verify the authenticity of the token"); + } + + await cache.RemoveAsync(GetPreAuthCacheKey(request.Fingerprint), cancellation); + + return preAuthTokenStruct.UserId; + } } \ No newline at end of file From d3a60d2a30f12d26c9212d8508d2e6f768a222dc Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:29:25 +0300 Subject: [PATCH 22/45] feat: add interface for gen access token --- Security/Common/Interfaces/IAccessToken.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Security/Common/Interfaces/IAccessToken.cs diff --git a/Security/Common/Interfaces/IAccessToken.cs b/Security/Common/Interfaces/IAccessToken.cs new file mode 100644 index 0000000..a2ebed2 --- /dev/null +++ b/Security/Common/Interfaces/IAccessToken.cs @@ -0,0 +1,9 @@ +using System; + +namespace Mirea.Api.Security.Common.Interfaces; + +public interface IAccessToken +{ + (string Token, DateTime ExpireIn) GenerateToken(string userId); + DateTimeOffset GetExpireDateTime(string token); +} \ No newline at end of file From f55d701ff330331bc635187f8163901f28b9ebac Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:30:00 +0300 Subject: [PATCH 23/45] feat: add sliding expiration for cache --- Security/Common/Interfaces/ICacheService.cs | 6 +++++- Security/Services/PreAuthService.cs | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Security/Common/Interfaces/ICacheService.cs b/Security/Common/Interfaces/ICacheService.cs index c2a419b..c2cb1e3 100644 --- a/Security/Common/Interfaces/ICacheService.cs +++ b/Security/Common/Interfaces/ICacheService.cs @@ -6,7 +6,11 @@ namespace Mirea.Api.Security.Common.Interfaces; public interface ICacheService { - Task SetAsync(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, CancellationToken cancellationToken = default); + Task SetAsync(string key, T value, + TimeSpan? absoluteExpirationRelativeToNow = null, + TimeSpan? slidingExpiration = null, + CancellationToken cancellationToken = default); + Task GetAsync(string key, CancellationToken cancellationToken = default); Task RemoveAsync(string key, CancellationToken cancellationToken = default); } diff --git a/Security/Services/PreAuthService.cs b/Security/Services/PreAuthService.cs index 1a0443e..948b997 100644 --- a/Security/Services/PreAuthService.cs +++ b/Security/Services/PreAuthService.cs @@ -14,7 +14,7 @@ public class PreAuthService(ICacheService cache) { public TimeSpan Lifetime { private get; init; } - private static string GeneratePreAuthToken() => Guid.NewGuid().ToString().Replace("-", "") + + private static string GeneratePreAuthToken() => Guid.NewGuid().ToString().Replace("-", "") + GeneratorKey.GenerateString(16); private static string GetPreAuthCacheKey(string fingerprint) => $"{fingerprint}_pre_auth_token"; @@ -35,8 +35,8 @@ public class PreAuthService(ICacheService cache) await cache.SetAsync( GetPreAuthCacheKey(request.Fingerprint), JsonSerializer.SerializeToUtf8Bytes(preAuthTokenStruct), - Lifetime, - cancellation); + absoluteExpirationRelativeToNow: Lifetime, + cancellationToken: cancellation); return new PreAuthTokenResponse { From 7df4c8e4b6d5f97a32cc8099b65fc4b4a888de5e Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:32:22 +0300 Subject: [PATCH 24/45] feat: add auth service --- Security/Services/AuthService.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Security/Services/AuthService.cs diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs new file mode 100644 index 0000000..97aca20 --- /dev/null +++ b/Security/Services/AuthService.cs @@ -0,0 +1,8 @@ +using System; + +namespace Mirea.Api.Security.Services; + +public class AuthService() +{ + public TimeSpan Lifetime { private get; init; } +} \ No newline at end of file From b25be758addb7e5207be39e5e7a28a2971cbb706 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:33:55 +0300 Subject: [PATCH 25/45] feat: add auth token --- Security/Common/Domain/AuthToken.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Security/Common/Domain/AuthToken.cs diff --git a/Security/Common/Domain/AuthToken.cs b/Security/Common/Domain/AuthToken.cs new file mode 100644 index 0000000..4572e62 --- /dev/null +++ b/Security/Common/Domain/AuthToken.cs @@ -0,0 +1,13 @@ +using System; + +namespace Mirea.Api.Security.Common.Domain; + +public class AuthToken +{ + public required string RefreshToken { get; set; } + public required string UserAgent { get; set; } + public required string Ip { get; set; } + public required string UserId { get; set; } + public required string AccessToken { get; set; } + public DateTime CreatedAt { get; set; } +} \ No newline at end of file From a3a42dd5c2fdfcadbffa51e9dfef4ac037894654 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:35:44 +0300 Subject: [PATCH 26/45] feat: add generate refresh token --- Security/Services/AuthService.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 97aca20..107b7e8 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -5,4 +5,7 @@ namespace Mirea.Api.Security.Services; public class AuthService() { public TimeSpan Lifetime { private get; init; } + + private static string GenerateRefreshToken() => Guid.NewGuid().ToString().Replace("-", "") + + GeneratorKey.GenerateString(32); } \ No newline at end of file From 4240ad8110d65c94fd908ef171749d0500a46e16 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:36:26 +0300 Subject: [PATCH 27/45] feat: add auth key for cache --- Security/Services/AuthService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 107b7e8..e13f565 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -8,4 +8,6 @@ public class AuthService() private static string GenerateRefreshToken() => Guid.NewGuid().ToString().Replace("-", "") + GeneratorKey.GenerateString(32); + + private static string GetAuthCacheKey(string fingerprint) => $"{fingerprint}_auth_token"; } \ No newline at end of file From 43011457d678e79212467c3923a146e873d9e534 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:50:47 +0300 Subject: [PATCH 28/45] feat: add wrap for save to cache --- Security/Services/AuthService.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index e13f565..90ceb14 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -1,8 +1,12 @@ -using System; +using Mirea.Api.Security.Common.Domain; +using Mirea.Api.Security.Common.Dto.Requests; +using Mirea.Api.Security.Common.Dto.Responses; +using Mirea.Api.Security.Common.Interfaces; +using System; namespace Mirea.Api.Security.Services; -public class AuthService() +public class AuthService(ICacheService cache) { public TimeSpan Lifetime { private get; init; } @@ -10,4 +14,11 @@ public class AuthService() GeneratorKey.GenerateString(32); private static string GetAuthCacheKey(string fingerprint) => $"{fingerprint}_auth_token"; + + private Task SetAuthTokenDataToCache(string fingerprint, AuthToken data, CancellationToken cancellation) => + cache.SetAsync( + GetAuthCacheKey(fingerprint), + JsonSerializer.SerializeToUtf8Bytes(data), + slidingExpiration: Lifetime, + cancellationToken: cancellation); } \ No newline at end of file From f3063c53221a27b4c093891706565227469faada Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:51:03 +0300 Subject: [PATCH 29/45] feat: add generate access token --- Security/Services/AuthService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 90ceb14..c0be65c 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -6,12 +6,14 @@ using System; namespace Mirea.Api.Security.Services; -public class AuthService(ICacheService cache) +public class AuthService(ICacheService cache, IAccessToken accessTokenService) { public TimeSpan Lifetime { private get; init; } private static string GenerateRefreshToken() => Guid.NewGuid().ToString().Replace("-", "") + GeneratorKey.GenerateString(32); + private (string Token, DateTime ExpireIn) GenerateAccessToken(string userId) => + accessTokenService.GenerateToken(userId); private static string GetAuthCacheKey(string fingerprint) => $"{fingerprint}_auth_token"; From 81f2f995b051fb881b076cc5d63789594458650e Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:51:32 +0300 Subject: [PATCH 30/45] feat: add generate auth token --- Security/Services/AuthService.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index c0be65c..471ba37 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -3,6 +3,10 @@ using Mirea.Api.Security.Common.Dto.Requests; using Mirea.Api.Security.Common.Dto.Responses; using Mirea.Api.Security.Common.Interfaces; using System; +using System.Security; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; namespace Mirea.Api.Security.Services; @@ -23,4 +27,31 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService) JsonSerializer.SerializeToUtf8Bytes(data), slidingExpiration: Lifetime, cancellationToken: cancellation); + + public async Task GenerateAuthTokensAsync(TokenRequest request, string preAuthToken, CancellationToken cancellation = default) + { + string userId = await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation); + + var refreshToken = GenerateRefreshToken(); + var accessToken = GenerateAccessToken(userId); + + var authTokenStruct = new AuthToken + { + CreatedAt = DateTime.UtcNow, + Ip = request.Ip, + RefreshToken = refreshToken, + UserAgent = request.UserAgent, + UserId = userId, + AccessToken = accessToken.Token + }; + + await SetAuthTokenDataToCache(request.Fingerprint, authTokenStruct, cancellation); + + return new AuthTokenResponse + { + AccessToken = accessToken.Token, + ExpiresIn = accessToken.ExpireIn, + RefreshToken = authTokenStruct.RefreshToken + }; + } } \ No newline at end of file From 79fb05d428526a1b57918a3871c6ee769aaf1243 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:54:45 +0300 Subject: [PATCH 31/45] feat: add token revocation --- Security/Common/Interfaces/IRevokedToken.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Security/Common/Interfaces/IRevokedToken.cs diff --git a/Security/Common/Interfaces/IRevokedToken.cs b/Security/Common/Interfaces/IRevokedToken.cs new file mode 100644 index 0000000..d8d9edc --- /dev/null +++ b/Security/Common/Interfaces/IRevokedToken.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Mirea.Api.Security.Common.Interfaces; + +public interface IRevokedToken +{ + Task AddTokenToRevokedAsync(string token, DateTimeOffset expiresIn); + Task IsTokenRevokedAsync(string token); +} \ No newline at end of file From 9dd505a608de358fdf08b520c3b770b21ce63b9e Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:55:13 +0300 Subject: [PATCH 32/45] feat: add auth token response --- Security/Common/Dto/Responses/AuthTokenResponse.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Security/Common/Dto/Responses/AuthTokenResponse.cs diff --git a/Security/Common/Dto/Responses/AuthTokenResponse.cs b/Security/Common/Dto/Responses/AuthTokenResponse.cs new file mode 100644 index 0000000..0c8a3d4 --- /dev/null +++ b/Security/Common/Dto/Responses/AuthTokenResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace Mirea.Api.Security.Common.Dto.Responses; + +public class AuthTokenResponse +{ + public required string AccessToken { get; set; } + public required string RefreshToken { get; set; } + public DateTime ExpiresIn { get; set; } +} \ No newline at end of file From 4138c7000757f6b1740d4295f88e1f7998cf25be Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:55:31 +0300 Subject: [PATCH 33/45] feat: add wrap for revoke access token --- Security/Services/AuthService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 471ba37..392e721 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace Mirea.Api.Security.Services; -public class AuthService(ICacheService cache, IAccessToken accessTokenService) +public class AuthService(ICacheService cache, IAccessToken accessTokenService, IRevokedToken revokedToken) { public TimeSpan Lifetime { private get; init; } @@ -28,6 +28,9 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService) slidingExpiration: Lifetime, cancellationToken: cancellation); + private Task RevokeAccessToken(string token) => + revokedToken.AddTokenToRevokedAsync(token, accessTokenService.GetExpireDateTime(token)); + public async Task GenerateAuthTokensAsync(TokenRequest request, string preAuthToken, CancellationToken cancellation = default) { string userId = await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation); From d84011cd71d71be0eb6e7238f99dac7c12f5140e Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:55:57 +0300 Subject: [PATCH 34/45] feat: add refresh token --- Security/Services/AuthService.cs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 392e721..9542249 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -57,4 +57,33 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I RefreshToken = authTokenStruct.RefreshToken }; } + + public async Task RefreshTokenAsync(TokenRequest request, string refreshToken, CancellationToken cancellation = default) + { + var authToken = await cache.GetAsync(GetAuthCacheKey(request.Fingerprint), cancellation) + ?? throw new SecurityException(request.Fingerprint); + + if (authToken.RefreshToken != refreshToken || + authToken.UserAgent != request.UserAgent && + authToken.Ip != request.Ip) + { + await cache.RemoveAsync(request.Fingerprint, cancellation); + await RevokeAccessToken(authToken.AccessToken); + + throw new SecurityException(request.Fingerprint); + } + + var accessToken = GenerateAccessToken(authToken.UserId); + await RevokeAccessToken(authToken.AccessToken); + + authToken.AccessToken = accessToken.Token; + await SetAuthTokenDataToCache(request.Fingerprint, authToken, cancellation); + + return new AuthTokenResponse + { + AccessToken = accessToken.Token, + ExpiresIn = accessToken.ExpireIn, + RefreshToken = GenerateRefreshToken() + }; + } } \ No newline at end of file From 61218c38a0acfd5d361861b18a158e289938162d Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 05:56:27 +0300 Subject: [PATCH 35/45] feat: add logout --- Security/Services/AuthService.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 9542249..58d439b 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -86,4 +86,14 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I RefreshToken = GenerateRefreshToken() }; } + + public async Task LogoutAsync(string fingerprint, CancellationToken cancellation = default) + { + var authTokenStruct = await cache.GetAsync(GetAuthCacheKey(fingerprint), cancellation); + if (authTokenStruct == null) return; + + await RevokeAccessToken(authTokenStruct.AccessToken); + + await cache.RemoveAsync(fingerprint, cancellation); + } } \ No newline at end of file From 25b6c7d69148c04d9a37abc4250fdceb6a2bc31a Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:00:15 +0300 Subject: [PATCH 36/45] feat: add method if there is no pre-auth token --- Security/Services/AuthService.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Security/Services/AuthService.cs b/Security/Services/AuthService.cs index 58d439b..5426532 100644 --- a/Security/Services/AuthService.cs +++ b/Security/Services/AuthService.cs @@ -31,10 +31,8 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I private Task RevokeAccessToken(string token) => revokedToken.AddTokenToRevokedAsync(token, accessTokenService.GetExpireDateTime(token)); - public async Task GenerateAuthTokensAsync(TokenRequest request, string preAuthToken, CancellationToken cancellation = default) + public async Task GenerateAuthTokensAsync(TokenRequest request, string userId, CancellationToken cancellation = default) { - string userId = await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation); - var refreshToken = GenerateRefreshToken(); var accessToken = GenerateAccessToken(userId); @@ -58,6 +56,12 @@ public class AuthService(ICacheService cache, IAccessToken accessTokenService, I }; } + public async Task GenerateAuthTokensWithPreAuthAsync(TokenRequest request, string preAuthToken, + CancellationToken cancellation = default) => + await GenerateAuthTokensAsync(request, + await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation), + cancellation); + public async Task RefreshTokenAsync(TokenRequest request, string refreshToken, CancellationToken cancellation = default) { var authToken = await cache.GetAsync(GetAuthCacheKey(request.Fingerprint), cancellation) From 2efdc6dbfe373914af7385c9d9a4a802636c9864 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:04:09 +0300 Subject: [PATCH 37/45] feat: add auth service to DI --- Security/DependencyInjection.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Security/DependencyInjection.cs b/Security/DependencyInjection.cs index 5441ebc..ed16c5e 100644 --- a/Security/DependencyInjection.cs +++ b/Security/DependencyInjection.cs @@ -26,7 +26,7 @@ public static class DependencyInjection Secret = configuration["SECURITY_HASH_TOKEN"] }); - var lifeTimeLogin = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_1_FA"]!)); + var lifeTimePreAuthToken = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_1_FA"]!)); services.AddSingleton(provider => { @@ -34,7 +34,21 @@ public static class DependencyInjection return new PreAuthService(cache) { - Lifetime = lifeTimeLogin + Lifetime = lifeTimePreAuthToken + }; + }); + + var lifeTimeRefreshToken = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_RT"]!)); + + services.AddSingleton(provider => + { + var cacheService = provider.GetRequiredService(); + var accessTokenService = provider.GetRequiredService(); + var revokedTokenService = provider.GetRequiredService(); + + return new AuthService(cacheService, accessTokenService, revokedTokenService) + { + Lifetime = lifeTimeRefreshToken }; }); From 9287acf7d269410ff1d981dfab29833cdad7e3ae Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:08:14 +0300 Subject: [PATCH 38/45] feat: add cache implementations depending on the type --- .../Security/DistributedCacheService.cs | 32 +++++++++++++++++ .../Services/Security/MemoryCacheService.cs | 34 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 Endpoint/Common/Services/Security/DistributedCacheService.cs create mode 100644 Endpoint/Common/Services/Security/MemoryCacheService.cs diff --git a/Endpoint/Common/Services/Security/DistributedCacheService.cs b/Endpoint/Common/Services/Security/DistributedCacheService.cs new file mode 100644 index 0000000..fa4c116 --- /dev/null +++ b/Endpoint/Common/Services/Security/DistributedCacheService.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Caching.Distributed; +using Mirea.Api.Security.Common.Interfaces; +using System.Text.Json; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace Mirea.Api.Endpoint.Common.Services.Security; + +public class DistributedCacheService(IDistributedCache cache) : ICacheService +{ + public async Task SetAsync(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default) + { + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow, + SlidingExpiration = slidingExpiration + }; + + var serializedValue = JsonSerializer.SerializeToUtf8Bytes(value); + await cache.SetAsync(key, serializedValue, options, cancellationToken); + } + + public async Task GetAsync(string key, CancellationToken cancellationToken = default) + { + var cachedValue = await cache.GetAsync(key, cancellationToken); + return cachedValue == null ? default : JsonSerializer.Deserialize(cachedValue); + } + + public Task RemoveAsync(string key, CancellationToken cancellationToken = default) => + cache.RemoveAsync(key, cancellationToken); +} \ No newline at end of file diff --git a/Endpoint/Common/Services/Security/MemoryCacheService.cs b/Endpoint/Common/Services/Security/MemoryCacheService.cs new file mode 100644 index 0000000..08d0a7f --- /dev/null +++ b/Endpoint/Common/Services/Security/MemoryCacheService.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Caching.Memory; +using Mirea.Api.Security.Common.Interfaces; +using System.Threading.Tasks; +using System.Threading; +using System; + +namespace Mirea.Api.Endpoint.Common.Services.Security; + +public class MemoryCacheService(IMemoryCache cache) : ICacheService +{ + public Task SetAsync(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, TimeSpan? slidingExpiration = null, CancellationToken cancellationToken = default) + { + var options = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow, + SlidingExpiration = slidingExpiration + }; + + cache.Set(key, value, options); + return Task.CompletedTask; + } + + public Task GetAsync(string key, CancellationToken cancellationToken = default) + { + cache.TryGetValue(key, out T? value); + return Task.FromResult(value); + } + + public Task RemoveAsync(string key, CancellationToken cancellationToken = default) + { + cache.Remove(key); + return Task.CompletedTask; + } +} \ No newline at end of file From 526bf5682b17f17995f3d2b47a867c3aa2c821f8 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:08:41 +0300 Subject: [PATCH 39/45] build: add security ref --- Endpoint/Endpoint.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Endpoint/Endpoint.csproj b/Endpoint/Endpoint.csproj index a1e08aa..3d4a2f6 100644 --- a/Endpoint/Endpoint.csproj +++ b/Endpoint/Endpoint.csproj @@ -32,6 +32,7 @@ + \ No newline at end of file From 6f02021fe7b12173c7614e933d7be4b6ab580d8f Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:11:18 +0300 Subject: [PATCH 40/45] feat: add revoked token service --- .../Security/MemoryRevokedTokenService.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs diff --git a/Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs b/Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs new file mode 100644 index 0000000..94c2f75 --- /dev/null +++ b/Endpoint/Common/Services/Security/MemoryRevokedTokenService.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Caching.Memory; +using Mirea.Api.Security.Common.Interfaces; +using System; +using System.Threading.Tasks; + +namespace Mirea.Api.Endpoint.Common.Services.Security; + +public class MemoryRevokedTokenService(IMemoryCache cache) : IRevokedToken +{ + public Task AddTokenToRevokedAsync(string token, DateTimeOffset expiresIn) + { + cache.Set(token, true, expiresIn); + return Task.CompletedTask; + } + + public Task IsTokenRevokedAsync(string token) => Task.FromResult(cache.TryGetValue(token, out _)); +} \ No newline at end of file From 62a859b44c550627b0c92d899ac32c55b8a205a5 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:11:29 +0300 Subject: [PATCH 41/45] style: clean code --- .../Common/Services/Security/DistributedCacheService.cs | 6 +++--- Endpoint/Common/Services/Security/MemoryCacheService.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Endpoint/Common/Services/Security/DistributedCacheService.cs b/Endpoint/Common/Services/Security/DistributedCacheService.cs index fa4c116..bf3dc39 100644 --- a/Endpoint/Common/Services/Security/DistributedCacheService.cs +++ b/Endpoint/Common/Services/Security/DistributedCacheService.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Caching.Distributed; using Mirea.Api.Security.Common.Interfaces; -using System.Text.Json; -using System.Threading.Tasks; -using System.Threading; using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; namespace Mirea.Api.Endpoint.Common.Services.Security; diff --git a/Endpoint/Common/Services/Security/MemoryCacheService.cs b/Endpoint/Common/Services/Security/MemoryCacheService.cs index 08d0a7f..a428034 100644 --- a/Endpoint/Common/Services/Security/MemoryCacheService.cs +++ b/Endpoint/Common/Services/Security/MemoryCacheService.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.Caching.Memory; using Mirea.Api.Security.Common.Interfaces; -using System.Threading.Tasks; -using System.Threading; using System; +using System.Threading; +using System.Threading.Tasks; namespace Mirea.Api.Endpoint.Common.Services.Security; From f2aa274d0af0b665beb17c32d8295fdd405af8bf Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:28:21 +0300 Subject: [PATCH 42/45] build: add jwt ref --- Endpoint/Endpoint.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Endpoint/Endpoint.csproj b/Endpoint/Endpoint.csproj index 3d4a2f6..985a730 100644 --- a/Endpoint/Endpoint.csproj +++ b/Endpoint/Endpoint.csproj @@ -23,9 +23,10 @@ - + + From 85802aa514a04677a1d9365de6d678a36dd246f5 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:28:42 +0300 Subject: [PATCH 43/45] feat: add jwt token service --- .../Services/Security/JwtTokenService.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Endpoint/Common/Services/Security/JwtTokenService.cs diff --git a/Endpoint/Common/Services/Security/JwtTokenService.cs b/Endpoint/Common/Services/Security/JwtTokenService.cs new file mode 100644 index 0000000..7c3225f --- /dev/null +++ b/Endpoint/Common/Services/Security/JwtTokenService.cs @@ -0,0 +1,82 @@ +using Microsoft.IdentityModel.Tokens; +using Mirea.Api.Security.Common.Interfaces; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; + +namespace Mirea.Api.Endpoint.Common.Services.Security; + +public class JwtTokenService : IAccessToken +{ + public required string Issuer { private get; init; } + public required string Audience { private get; init; } + public TimeSpan Lifetime { private get; init; } + + public ReadOnlyMemory EncryptionKey { get; init; } + public ReadOnlyMemory SigningKey { private get; init; } + + public (string Token, DateTime ExpireIn) GenerateToken(string userId) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var signingKey = new SymmetricSecurityKey(SigningKey.ToArray()); + var encryptionKey = new SymmetricSecurityKey(EncryptionKey.ToArray()); + var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha512); + + var expires = DateTime.UtcNow.Add(Lifetime); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Issuer = Issuer, + Audience = Audience, + Expires = expires, + SigningCredentials = signingCredentials, + Subject = new ClaimsIdentity( + [ + new Claim(ClaimTypes.Name, userId), + // todo: get role by userId + new Claim(ClaimTypes.Role, "") + ]), + EncryptingCredentials = new EncryptingCredentials(encryptionKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + + return (tokenHandler.WriteToken(token), expires); + } + + public DateTimeOffset GetExpireDateTime(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var signingKey = new SymmetricSecurityKey(SigningKey.ToArray()); + var encryptionKey = new SymmetricSecurityKey(EncryptionKey.ToArray()); + + var tokenValidationParameters = new TokenValidationParameters + { + ValidIssuer = Issuer, + ValidAudience = Audience, + IssuerSigningKey = signingKey, + TokenDecryptionKey = encryptionKey, + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = false + }; + + try + { + var claimsPrincipal = tokenHandler.ValidateToken(token, tokenValidationParameters, out _); + + var expClaim = claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "exp"); + + if (expClaim != null && long.TryParse(expClaim.Value, out var expUnix)) + return DateTimeOffset.FromUnixTimeSeconds(expUnix); + } + catch (SecurityTokenException) + { + return DateTimeOffset.MinValue; + } + + return DateTimeOffset.MinValue; + } +} \ No newline at end of file From 38ec80a566fc55aef362a988d9335b61524ba9bf Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:30:01 +0300 Subject: [PATCH 44/45] feat: add configuration for jwt token --- Endpoint/Program.cs | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/Endpoint/Program.cs b/Endpoint/Program.cs index 89d98ab..adeeb99 100644 --- a/Endpoint/Program.cs +++ b/Endpoint/Program.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -6,20 +7,24 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Mirea.Api.DataAccess.Application; using Mirea.Api.DataAccess.Persistence; using Mirea.Api.Endpoint.Common.Interfaces; using Mirea.Api.Endpoint.Common.Services; +using Mirea.Api.Endpoint.Common.Services.Security; using Mirea.Api.Endpoint.Configuration; using Mirea.Api.Endpoint.Configuration.General; using Mirea.Api.Endpoint.Configuration.General.Validators; using Mirea.Api.Endpoint.Configuration.Swagger; using Mirea.Api.Endpoint.Middleware; +using Mirea.Api.Security.Common.Interfaces; using Swashbuckle.AspNetCore.SwaggerGen; using System; using System.Collections; using System.IO; using System.Linq; +using System.Text; namespace Mirea.Api.Endpoint; @@ -40,6 +45,58 @@ public class Program return result.Build(); } + private static IServiceCollection ConfigureJwtToken(IServiceCollection services, IConfiguration configuration) + { + var lifeTimeJwt = TimeSpan.FromMinutes(int.Parse(configuration["SECURITY_LIFE_TIME_JWT"]!)); + + var jwtDecrypt = Encoding.UTF8.GetBytes(configuration["SECURITY_ENCRYPTION_TOKEN"] ?? string.Empty); + + if (jwtDecrypt.Length != 32) + throw new InvalidOperationException("The secret token \"SECURITY_ENCRYPTION_TOKEN\" cannot be less than 32 characters long. Now the size is equal is " + jwtDecrypt.Length); + + var jwtKey = Encoding.UTF8.GetBytes(configuration["SECURITY_SIGNING_TOKEN"] ?? string.Empty); + + if (jwtKey.Length != 64) + throw new InvalidOperationException("The signature token \"SECURITY_SIGNING_TOKEN\" cannot be less than 64 characters. Now the size is " + jwtKey.Length); + + var jwtIssuer = configuration["SECURITY_JWT_ISSUER"]; + var jwtAudience = configuration["SECURITY_JWT_AUDIENCE"]; + + if (string.IsNullOrEmpty(jwtAudience) || string.IsNullOrEmpty(jwtIssuer)) + throw new InvalidOperationException("The \"SECURITY_JWT_ISSUER\" and \"SECURITY_JWT_AUDIENCE\" are not specified"); + + services.AddSingleton(_ => new JwtTokenService + { + Audience = jwtAudience, + Issuer = jwtIssuer, + Lifetime = lifeTimeJwt, + EncryptionKey = jwtDecrypt, + SigningKey = jwtKey + }); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = jwtIssuer, + + ValidateAudience = true, + ValidAudience = jwtAudience, + + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(jwtKey), + TokenDecryptionKey = new SymmetricSecurityKey(jwtDecrypt) + }; + }); + + return services; + } public static void Main(string[] args) { Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); From d2ef99d0b29cbfc3036b4029b52ffafdb4e82011 Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 29 May 2024 06:42:14 +0300 Subject: [PATCH 45/45] feat: add security configure --- Endpoint/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Endpoint/Program.cs b/Endpoint/Program.cs index adeeb99..840504f 100644 --- a/Endpoint/Program.cs +++ b/Endpoint/Program.cs @@ -97,6 +97,14 @@ public class Program return services; } + + private static IServiceCollection ConfigureSecurity(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + return services; + } public static void Main(string[] args) { Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);