1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Timeline.Configs;
namespace Timeline.Services
{
public class TokenInfo
{
public long Id { get; set; }
public long Version { get; set; }
}
public interface IJwtService
{
/// <summary>
/// Create a JWT token for a given token info.
/// </summary>
/// <param name="tokenInfo">The info to generate token.</param>
/// <param name="expires">The expire time. If null then use current time with offset in config.</param>
/// <returns>Return the generated token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null);
/// <summary>
/// Verify a JWT token.
/// Return null is <paramref name="token"/> is null.
/// </summary>
/// <param name="token">The token string to verify.</param>
/// <returns>Return the saved info in token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
/// <exception cref="JwtVerifyException">Thrown when the token is invalid.</exception>
TokenInfo VerifyJwtToken(string token);
}
public class JwtService : IJwtService
{
private const string VersionClaimType = "timeline_version";
private readonly IOptionsMonitor<JwtConfig> _jwtConfig;
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
private readonly IClock _clock;
public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
{
_jwtConfig = jwtConfig;
_clock = clock;
}
public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null)
{
if (tokenInfo == null)
throw new ArgumentNullException(nameof(tokenInfo));
var config = _jwtConfig.CurrentValue;
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64));
identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(CultureInfo.InvariantCulture.NumberFormat), ClaimValueTypes.Integer64));
var tokenDescriptor = new SecurityTokenDescriptor()
{
Subject = identity,
Issuer = config.Issuer,
Audience = config.Audience,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384),
IssuedAt = _clock.GetCurrentTime(),
Expires = expires.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)),
NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass.
};
var token = _tokenHandler.CreateToken(tokenDescriptor);
var tokenString = _tokenHandler.WriteToken(token);
return tokenString;
}
public TokenInfo VerifyJwtToken(string token)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
var config = _jwtConfig.CurrentValue;
try
{
var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = config.Issuer,
ValidAudience = config.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
}, out _);
var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (idClaim == null)
throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim);
if (!long.TryParse(idClaim, out var id))
throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat);
var versionClaim = principal.FindFirstValue(VersionClaimType);
if (versionClaim == null)
throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim);
if (!long.TryParse(versionClaim, out var version))
throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat);
return new TokenInfo
{
Id = id,
Version = version
};
}
catch (SecurityTokenExpiredException e)
{
throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired);
}
catch (Exception e)
{
throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others);
}
}
}
}
|