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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
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; }
}
[Serializable]
public class JwtTokenVerifyException : Exception
{
public static class ErrorCodes
{
// Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server.
public const int Others = -1001;
public const int NoIdClaim = -1002;
public const int IdClaimBadFormat = -1003;
public const int NoVersionClaim = -1004;
public const int VersionClaimBadFormat = -1005;
/// <summary>
/// Corresponds to <see cref="SecurityTokenExpiredException"/>.
/// </summary>
public const int Expired = -2001;
}
public JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; }
public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; }
public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; }
public JwtTokenVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; }
protected JwtTokenVerifyException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
public int ErrorCode { get; private set; }
private static string GetErrorMessage(int errorCode)
{
switch (errorCode)
{
case ErrorCodes.Others:
return "Uncommon error, see inner exception for more information.";
case ErrorCodes.NoIdClaim:
return "Id claim does not exist.";
case ErrorCodes.IdClaimBadFormat:
return "Id claim is not a number.";
case ErrorCodes.NoVersionClaim:
return "Version claim does not exist.";
case ErrorCodes.VersionClaimBadFormat:
return "Version claim is not a number";
case ErrorCodes.Expired:
return "Token is expired.";
default:
return "Unknown error code.";
}
}
}
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="JwtTokenVerifyException">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(), ClaimValueTypes.Integer64));
identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), 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))
};
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 JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoIdClaim);
if (!long.TryParse(idClaim, out var id))
throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat);
var versionClaim = principal.FindFirstValue(VersionClaimType);
if (versionClaim == null)
throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoVersionClaim);
if (!long.TryParse(versionClaim, out var version))
throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.VersionClaimBadFormat);
return new TokenInfo
{
Id = id,
Version = version
};
}
catch (SecurityTokenExpiredException e)
{
throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Expired);
}
catch (Exception e)
{
throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Others);
}
}
}
}
|