68 lines
2.9 KiB
C#
68 lines
2.9 KiB
C#
using QuestionsApi.Models;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace QuestionsApi.Services;
|
|
|
|
public sealed class DynamicQuestionTokenService
|
|
{
|
|
private readonly byte[] _key;
|
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
|
|
|
public DynamicQuestionTokenService(IConfiguration configuration)
|
|
{
|
|
var key = configuration["DynamicQuestions:SigningKey"]
|
|
?? configuration["Jwt:Key"]
|
|
?? "development-dynamic-question-signing-key-change-me";
|
|
_key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
|
}
|
|
|
|
public string SignGeneration(DynamicGenerationClaims claims) => Sign(claims);
|
|
public string SignRun(DynamicRunClaims claims) => Sign(claims);
|
|
public DynamicGenerationClaims ReadGeneration(string token, string userId) => Read<DynamicGenerationClaims>(token, userId);
|
|
public DynamicRunClaims ReadRun(string token, string userId) => Read<DynamicRunClaims>(token, userId);
|
|
|
|
private string Sign<T>(T value)
|
|
{
|
|
var payload = Base64Url(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, Json)));
|
|
using var hmac = new HMACSHA256(_key);
|
|
return $"{payload}.{Base64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)))}";
|
|
}
|
|
|
|
private T Read<T>(string token, string userId) where T : class
|
|
{
|
|
var parts = (token ?? "").Split('.');
|
|
if (parts.Length != 2) throw new ArgumentException("token is invalid");
|
|
using var hmac = new HMACSHA256(_key);
|
|
var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(parts[0]));
|
|
var supplied = FromBase64Url(parts[1]);
|
|
if (!CryptographicOperations.FixedTimeEquals(expected, supplied)) throw new ArgumentException("token is invalid");
|
|
var value = JsonSerializer.Deserialize<T>(FromBase64Url(parts[0]), Json)
|
|
?? throw new ArgumentException("token is invalid");
|
|
var tokenUser = value switch
|
|
{
|
|
DynamicGenerationClaims generation => generation.UserId,
|
|
DynamicRunClaims run => run.UserId,
|
|
_ => ""
|
|
};
|
|
var expires = value switch
|
|
{
|
|
DynamicGenerationClaims generation => generation.ExpiresUtc,
|
|
DynamicRunClaims run => run.ExpiresUtc,
|
|
_ => DateTime.MinValue
|
|
};
|
|
if (!string.Equals(tokenUser, userId, StringComparison.Ordinal) || expires < DateTime.UtcNow)
|
|
throw new ArgumentException("token is expired or belongs to another user");
|
|
return value;
|
|
}
|
|
|
|
private static string Base64Url(byte[] value) => Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
private static byte[] FromBase64Url(string value)
|
|
{
|
|
value = value.Replace('-', '+').Replace('_', '/');
|
|
value += new string('=', (4 - value.Length % 4) % 4);
|
|
return Convert.FromBase64String(value);
|
|
}
|
|
}
|