Compare commits
15 Commits
470031af39
...
25b6c7d691
Author | SHA1 | Date | |
---|---|---|---|
25b6c7d691 | |||
61218c38a0 | |||
d84011cd71 | |||
4138c70007 | |||
9dd505a608 | |||
79fb05d428 | |||
81f2f995b0 | |||
f3063c5322 | |||
43011457d6 | |||
4240ad8110 | |||
a3a42dd5c2 | |||
b25be758ad | |||
7df4c8e4b6 | |||
f55d701ff3 | |||
d3a60d2a30 |
13
Security/Common/Domain/AuthToken.cs
Normal file
13
Security/Common/Domain/AuthToken.cs
Normal file
@ -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; }
|
||||
}
|
10
Security/Common/Dto/Responses/AuthTokenResponse.cs
Normal file
10
Security/Common/Dto/Responses/AuthTokenResponse.cs
Normal file
@ -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; }
|
||||
}
|
9
Security/Common/Interfaces/IAccessToken.cs
Normal file
9
Security/Common/Interfaces/IAccessToken.cs
Normal file
@ -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);
|
||||
}
|
@ -6,7 +6,11 @@ namespace Mirea.Api.Security.Common.Interfaces;
|
||||
|
||||
public interface ICacheService
|
||||
{
|
||||
Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpirationRelativeToNow = null, CancellationToken cancellationToken = default);
|
||||
Task SetAsync<T>(string key, T value,
|
||||
TimeSpan? absoluteExpirationRelativeToNow = null,
|
||||
TimeSpan? slidingExpiration = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
|
||||
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
10
Security/Common/Interfaces/IRevokedToken.cs
Normal file
10
Security/Common/Interfaces/IRevokedToken.cs
Normal file
@ -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<bool> IsTokenRevokedAsync(string token);
|
||||
}
|
103
Security/Services/AuthService.cs
Normal file
103
Security/Services/AuthService.cs
Normal file
@ -0,0 +1,103 @@
|
||||
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.Security;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Mirea.Api.Security.Services;
|
||||
|
||||
public class AuthService(ICacheService cache, IAccessToken accessTokenService, IRevokedToken revokedToken)
|
||||
{
|
||||
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";
|
||||
|
||||
private Task SetAuthTokenDataToCache(string fingerprint, AuthToken data, CancellationToken cancellation) =>
|
||||
cache.SetAsync(
|
||||
GetAuthCacheKey(fingerprint),
|
||||
JsonSerializer.SerializeToUtf8Bytes(data),
|
||||
slidingExpiration: Lifetime,
|
||||
cancellationToken: cancellation);
|
||||
|
||||
private Task RevokeAccessToken(string token) =>
|
||||
revokedToken.AddTokenToRevokedAsync(token, accessTokenService.GetExpireDateTime(token));
|
||||
|
||||
public async Task<AuthTokenResponse> GenerateAuthTokensAsync(TokenRequest request, string userId, CancellationToken cancellation = default)
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AuthTokenResponse> GenerateAuthTokensWithPreAuthAsync(TokenRequest request, string preAuthToken,
|
||||
CancellationToken cancellation = default) =>
|
||||
await GenerateAuthTokensAsync(request,
|
||||
await new PreAuthService(cache).MatchToken(request, preAuthToken, cancellation),
|
||||
cancellation);
|
||||
|
||||
public async Task<AuthTokenResponse> RefreshTokenAsync(TokenRequest request, string refreshToken, CancellationToken cancellation = default)
|
||||
{
|
||||
var authToken = await cache.GetAsync<AuthToken>(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()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(string fingerprint, CancellationToken cancellation = default)
|
||||
{
|
||||
var authTokenStruct = await cache.GetAsync<AuthToken>(GetAuthCacheKey(fingerprint), cancellation);
|
||||
if (authTokenStruct == null) return;
|
||||
|
||||
await RevokeAccessToken(authTokenStruct.AccessToken);
|
||||
|
||||
await cache.RemoveAsync(fingerprint, cancellation);
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user