Files
step3/api/QuestionsApi/Services/DynamicQuestionTokenService.cs
T

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);
}
}