Forge
csharp4405de34
1using System.IdentityModel.Tokens.Jwt;
2using System.Security.Claims;
3using System.Security.Cryptography;
4using System.Text;
5using IntercomService.Contracts;
6using IntercomService.Data;
7using IntercomService.Options;
8using Microsoft.EntityFrameworkCore;
9using Microsoft.Extensions.Options;
10using Microsoft.IdentityModel.Tokens;
11
12namespace IntercomService.Services;
13
14public sealed class JwtTokenService(
15 IntercomDbContext db,
16 IOptions<JwtOptions> jwtOptions)
17{
18 private readonly JwtOptions _jwt = jwtOptions.Value;
19
20 public async Task<TokenResponse> IssueTokensAsync(MemberEntity member, CancellationToken ct)
21 {
22 var access = CreateAccessToken(member);
23 var refreshPlain = GenerateRefreshToken();
24 var refreshHash = HashToken(refreshPlain);
25
26 db.RefreshTokens.Add(new RefreshTokenEntity
27 {
28 Id = Guid.NewGuid().ToString("N"),
29 MemberId = member.MemberId,
30 TokenHash = refreshHash,
31 ExpiresAtUtc = DateTimeOffset.UtcNow.AddDays(_jwt.RefreshTokenDays),
32 });
33 await db.SaveChangesAsync(ct).ConfigureAwait(false);
34
35 return new TokenResponse(access, refreshPlain, _jwt.AccessTokenMinutes * 60);
36 }
37
38 public async Task<TokenResponse?> RefreshAsync(string refreshToken, CancellationToken ct)
39 {
40 var hash = HashToken(refreshToken);
41 var row = await db.RefreshTokens
42 .Include(x => x.Member)
43 .FirstOrDefaultAsync(x => x.TokenHash == hash && x.RevokedAtUtc == null, ct)
44 .ConfigureAwait(false);
45
46 if (row?.Member is null || row.ExpiresAtUtc < DateTimeOffset.UtcNow)
47 return null;
48
49 row.RevokedAtUtc = DateTimeOffset.UtcNow;
50 await db.SaveChangesAsync(ct).ConfigureAwait(false);
51 return await IssueTokensAsync(row.Member, ct).ConfigureAwait(false);
52 }
53
54 public async Task RevokeRefreshAsync(string refreshToken, CancellationToken ct)
55 {
56 var hash = HashToken(refreshToken);
57 var row = await db.RefreshTokens.FirstOrDefaultAsync(x => x.TokenHash == hash, ct).ConfigureAwait(false);
58 if (row is null)
59 return;
60 row.RevokedAtUtc = DateTimeOffset.UtcNow;
61 await db.SaveChangesAsync(ct).ConfigureAwait(false);
62 }
63
64 public string CreateAccessToken(MemberEntity member)
65 {
66 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.SigningKey));
67 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
68 var claims = new[]
69 {
70 new Claim(JwtRegisteredClaimNames.Sub, member.MemberId),
71 new Claim("display_name", member.DisplayName),
72 new Claim("member_kind", member.MemberKind),
73 };
74
75 var token = new JwtSecurityToken(
76 issuer: _jwt.Issuer,
77 audience: _jwt.Audience,
78 claims: claims,
79 expires: DateTime.UtcNow.AddMinutes(_jwt.AccessTokenMinutes),
80 signingCredentials: creds);
81
82 return new JwtSecurityTokenHandler().WriteToken(token);
83 }
84
85 public static string HashToken(string token)
86 {
87 var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
88 return Convert.ToHexString(bytes);
89 }
90
91 private static string GenerateRefreshToken() =>
92 Convert.ToBase64String(RandomNumberGenerator.GetBytes(48));
93}
94
View only · write via MCP/CIDE