Add data-driven dynamic question mode
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user