Compare commits

...

7 Commits

5 changed files with 66 additions and 30 deletions

View File

@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence", "Persistence\
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiDto", "ApiDto\ApiDto.csproj", "{0335FA36-E137-453F-853B-916674C168FE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Security", "Security\Security.csproj", "{47A3C065-4E1D-4B1E-AAB4-2BB8F40E56B4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{0335FA36-E137-453F-853B-916674C168FE}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -5,5 +5,6 @@ public class PreAuthToken
public required string Fingerprint { get; set; } public required string Fingerprint { get; set; }
public required string UserAgent { get; set; } public required string UserAgent { get; set; }
public required string UserId { get; set; } public required string UserId { get; set; }
public required string Ip { get; set; }
public required string Token { get; set; } public required string Token { get; set; }
} }

View File

@ -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<byte> 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<byte> utf8Bytes = new byte[Base64.GetMaxEncodedToUtf8Length(randomBytes.Length)];
Base64.EncodeToUtf8(randomBytes, utf8Bytes, out _, out _);
return Encoding.UTF8.GetString(utf8Bytes);
}
}

View File

@ -1,6 +1,5 @@
using Konscious.Security.Cryptography; using Konscious.Security.Cryptography;
using System; using System;
using System.Buffers.Text;
using System.Text; using System.Text;
namespace Mirea.Api.Security.Services; namespace Mirea.Api.Security.Services;
@ -41,29 +40,9 @@ public class PasswordHashService
return result == 0; return result == 0;
} }
public static ReadOnlySpan<byte> 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<byte> 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) public (string Salt, string Hash) HashPassword(string password)
{ {
var salt = GenerateRandomKeyBytes(SaltSize); var salt = GeneratorKey.GenerateBytes(SaltSize);
var hash = HashPassword(password, salt); var hash = HashPassword(password, salt);
return (Convert.ToBase64String(salt), Convert.ToBase64String(hash)); return (Convert.ToBase64String(salt), Convert.ToBase64String(hash));

View File

@ -3,6 +3,7 @@ using Mirea.Api.Security.Common.Dto.Requests;
using Mirea.Api.Security.Common.Dto.Responses; using Mirea.Api.Security.Common.Dto.Responses;
using Mirea.Api.Security.Common.Interfaces; using Mirea.Api.Security.Common.Interfaces;
using System; using System;
using System.Security;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,30 +14,51 @@ public class PreAuthService(ICacheService cache)
{ {
public TimeSpan Lifetime { private get; init; } 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<PreAuthTokenResponse> CreateLoginTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default) private static string GetPreAuthCacheKey(string fingerprint) => $"{fingerprint}_pre_auth_token";
public async Task<PreAuthTokenResponse> GeneratePreAuthTokenAsync(TokenRequest request, string userId, CancellationToken cancellation = default)
{ {
var firstAuthToken = GenerateFirstAuthToken(); var preAuthToken = GeneratePreAuthToken();
var loginStructure = new PreAuthToken var preAuthTokenStruct = new PreAuthToken
{ {
Fingerprint = request.Fingerprint, Fingerprint = request.Fingerprint,
UserId = userId, UserId = userId,
UserAgent = request.UserAgent, UserAgent = request.UserAgent,
Token = firstAuthToken Token = preAuthToken,
Ip = request.Ip
}; };
await cache.SetAsync( await cache.SetAsync(
request.Fingerprint, GetPreAuthCacheKey(request.Fingerprint),
JsonSerializer.SerializeToUtf8Bytes(loginStructure), JsonSerializer.SerializeToUtf8Bytes(preAuthTokenStruct),
Lifetime, Lifetime,
cancellation); cancellation);
return new PreAuthTokenResponse return new PreAuthTokenResponse
{ {
Token = firstAuthToken, Token = preAuthToken,
ExpiresIn = DateTime.UtcNow.Add(Lifetime) ExpiresIn = DateTime.UtcNow.Add(Lifetime)
}; };
} }
public async Task<string> MatchToken(TokenRequest request, string preAuthToken, CancellationToken cancellation = default)
{
var preAuthTokenStruct = await cache.GetAsync<PreAuthToken>(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;
}
} }