Compare commits
7 Commits
8408b80c35
...
470031af39
Author | SHA1 | Date | |
---|---|---|---|
470031af39 | |||
916b3795ed | |||
f4ad1518ef | |||
ac7bbde75e | |||
47a57693f8 | |||
d05ba5349f | |||
5fde5bd396 |
@ -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
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
28
Security/Services/GeneratorKey.cs
Normal file
28
Security/Services/GeneratorKey.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user