aboutsummaryrefslogtreecommitdiff
path: root/Timeline/Services
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-01-21 01:11:17 +0800
committercrupest <crupest@outlook.com>2020-01-21 01:11:17 +0800
commit747bf829351c30069647a44f98ac19f1a214370f (patch)
treea45506852659b9d8e2bfe0b9e58a496060f7cd9b /Timeline/Services
parent40eea04e1ec9b71c5215e9dce5a6963ea60cafaa (diff)
downloadtimeline-747bf829351c30069647a44f98ac19f1a214370f.tar.gz
timeline-747bf829351c30069647a44f98ac19f1a214370f.tar.bz2
timeline-747bf829351c30069647a44f98ac19f1a214370f.zip
...
Diffstat (limited to 'Timeline/Services')
-rw-r--r--Timeline/Services/DatabaseExtensions.cs2
-rw-r--r--Timeline/Services/JwtUserTokenBadFormatException.cs48
-rw-r--r--Timeline/Services/JwtVerifyException.cs59
-rw-r--r--Timeline/Services/UserAvatarService.cs2
-rw-r--r--Timeline/Services/UserDetailService.cs2
-rw-r--r--Timeline/Services/UserService.cs68
-rw-r--r--Timeline/Services/UserTokenException.cs71
-rw-r--r--Timeline/Services/UserTokenManager.cs93
-rw-r--r--Timeline/Services/UserTokenService.cs (renamed from Timeline/Services/JwtService.cs)81
-rw-r--r--Timeline/Services/UsernameBadFormatException.cs2
10 files changed, 286 insertions, 142 deletions
diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs
index 140c3146..c5c96d8c 100644
--- a/Timeline/Services/DatabaseExtensions.cs
+++ b/Timeline/Services/DatabaseExtensions.cs
@@ -19,7 +19,7 @@ namespace Timeline.Services
/// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
/// <exception cref="UsernameBadFormatException">Thrown if <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
- internal static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string? username)
+ internal static async Task<long> CheckAndGetUser(DbSet<UserEntity> userDbSet, string? username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
diff --git a/Timeline/Services/JwtUserTokenBadFormatException.cs b/Timeline/Services/JwtUserTokenBadFormatException.cs
new file mode 100644
index 00000000..c528c3e3
--- /dev/null
+++ b/Timeline/Services/JwtUserTokenBadFormatException.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Globalization;
+using static Timeline.Resources.Services.Exception;
+
+namespace Timeline.Services
+{
+ [Serializable]
+ public class JwtUserTokenBadFormatException : UserTokenBadFormatException
+ {
+ public enum ErrorKind
+ {
+ NoIdClaim,
+ IdClaimBadFormat,
+ NoVersionClaim,
+ VersionClaimBadFormat,
+ Other
+ }
+
+ public JwtUserTokenBadFormatException() : this("", ErrorKind.Other) { }
+ public JwtUserTokenBadFormatException(string message) : base(message) { }
+ public JwtUserTokenBadFormatException(string message, Exception inner) : base(message, inner) { }
+
+ public JwtUserTokenBadFormatException(string token, ErrorKind type) : base(token, GetErrorMessage(type)) { ErrorType = type; }
+ public JwtUserTokenBadFormatException(string token, ErrorKind type, Exception inner) : base(token, GetErrorMessage(type), inner) { ErrorType = type; }
+ public JwtUserTokenBadFormatException(string token, ErrorKind type, string message, Exception inner) : base(token, message, inner) { ErrorType = type; }
+ protected JwtUserTokenBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public ErrorKind ErrorType { get; set; }
+
+ private static string GetErrorMessage(ErrorKind type)
+ {
+ var reason = type switch
+ {
+ ErrorKind.NoIdClaim => JwtUserTokenBadFormatExceptionIdMissing,
+ ErrorKind.IdClaimBadFormat => JwtUserTokenBadFormatExceptionIdBadFormat,
+ ErrorKind.NoVersionClaim => JwtUserTokenBadFormatExceptionVersionMissing,
+ ErrorKind.VersionClaimBadFormat => JwtUserTokenBadFormatExceptionVersionBadFormat,
+ ErrorKind.Other => JwtUserTokenBadFormatExceptionOthers,
+ _ => JwtUserTokenBadFormatExceptionUnknown
+ };
+
+ return string.Format(CultureInfo.CurrentCulture,
+ Resources.Services.Exception.JwtUserTokenBadFormatException, reason);
+ }
+ }
+}
diff --git a/Timeline/Services/JwtVerifyException.cs b/Timeline/Services/JwtVerifyException.cs
deleted file mode 100644
index a915b51a..00000000
--- a/Timeline/Services/JwtVerifyException.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Microsoft.IdentityModel.Tokens;
-using System;
-using System.Globalization;
-using static Timeline.Resources.Services.Exception;
-
-namespace Timeline.Services
-{
- [Serializable]
- public class JwtVerifyException : 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 const int OldVersion = -2002;
- }
-
- public JwtVerifyException() : base(GetErrorMessage(0)) { }
- public JwtVerifyException(string message) : base(message) { }
- public JwtVerifyException(string message, Exception inner) : base(message, inner) { }
-
- public JwtVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; }
- public JwtVerifyException(string message, int code) : base(message) { ErrorCode = code; }
- public JwtVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; }
- public JwtVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; }
- protected JwtVerifyException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public int ErrorCode { get; set; }
-
- private static string GetErrorMessage(int errorCode)
- {
- var reason = errorCode switch
- {
- ErrorCodes.Others => JwtVerifyExceptionOthers,
- ErrorCodes.NoIdClaim => JwtVerifyExceptionNoIdClaim,
- ErrorCodes.IdClaimBadFormat => JwtVerifyExceptionIdClaimBadFormat,
- ErrorCodes.NoVersionClaim => JwtVerifyExceptionNoVersionClaim,
- ErrorCodes.VersionClaimBadFormat => JwtVerifyExceptionVersionClaimBadFormat,
- ErrorCodes.Expired => JwtVerifyExceptionExpired,
- ErrorCodes.OldVersion => JwtVerifyExceptionOldVersion,
- _ => JwtVerifyExceptionUnknown
- };
-
- return string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.JwtVerifyException, reason);
- }
- }
-}
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs
index 01201864..ac7dd857 100644
--- a/Timeline/Services/UserAvatarService.cs
+++ b/Timeline/Services/UserAvatarService.cs
@@ -275,7 +275,7 @@ namespace Timeline.Services
var create = avatarEntity == null;
if (create)
{
- avatarEntity = new UserAvatar();
+ avatarEntity = new UserAvatarEntity();
}
avatarEntity!.Type = avatar.Type;
avatarEntity.Data = avatar.Data;
diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs
index 0b24e4e2..4f4a7942 100644
--- a/Timeline/Services/UserDetailService.cs
+++ b/Timeline/Services/UserDetailService.cs
@@ -77,7 +77,7 @@ namespace Timeline.Services
var create = userDetail == null;
if (create)
{
- userDetail = new UserDetail
+ userDetail = new UserDetailEntity
{
UserId = userId
};
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 4012539f..db2350a2 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -11,47 +11,37 @@ using Timeline.Models.Validation;
namespace Timeline.Services
{
- public class CreateTokenResult
- {
- public string Token { get; set; } = default!;
- public UserInfo User { get; set; } = default!;
- }
-
public interface IUserService
{
/// <summary>
- /// Try to anthenticate with the given username and password.
- /// If success, create a token and return the user info.
+ /// Try to verify the given username and password.
/// </summary>
- /// <param name="username">The username of the user to anthenticate.</param>
- /// <param name="password">The password of the user to anthenticate.</param>
- /// <param name="expires">The expired time point. Null then use default. See <see cref="JwtService.GenerateJwtToken(TokenInfo, DateTime?)"/> for what is default.</param>
- /// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
+ /// <param name="username">The username of the user to verify.</param>
+ /// <param name="password">The password of the user to verify.</param>
+ /// <returns>The user info.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
/// <exception cref="UsernameBadFormatException">Thrown when username is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
/// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
- Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires = null);
+ Task<UserInfo> VerifyCredential(string username, string password);
/// <summary>
- /// Verify the given token.
- /// If success, return the user info.
+ /// Try to get a user by id.
/// </summary>
- /// <param name="token">The token to verify.</param>
- /// <returns>The user info specified by the token.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
- /// <exception cref="JwtVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
- /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
- Task<UserInfo> VerifyToken(string token);
+ /// <param name="id">The id of the user.</param>
+ /// <returns>The user info.</returns>
+ /// <exception cref="UserNotExistException">Thrown when the user with given id does not exist.</exception>
+ Task<UserInfo> GetUserById(long id);
/// <summary>
/// Get the user info of given username.
/// </summary>
/// <param name="username">Username of the user.</param>
- /// <returns>The info of the user. Null if the user of given username does not exists.</returns>
+ /// <returns>The info of the user.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> is null.</exception>
/// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
- Task<UserInfo> GetUser(string username);
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ Task<UserInfo> GetUserByUsername(string username);
/// <summary>
/// List all users.
@@ -120,39 +110,24 @@ namespace Timeline.Services
Task ChangeUsername(string oldUsername, string newUsername);
}
- internal class UserCache
- {
- public string Username { get; set; } = default!;
- public bool Administrator { get; set; }
- public long Version { get; set; }
-
- public UserInfo ToUserInfo()
- {
- return new UserInfo(Username, Administrator);
- }
- }
-
public class UserService : IUserService
{
private readonly ILogger<UserService> _logger;
- private readonly IMemoryCache _memoryCache;
private readonly DatabaseContext _databaseContext;
- private readonly IJwtService _jwtService;
+ private readonly IMemoryCache _memoryCache;
+
private readonly IPasswordService _passwordService;
- private readonly UsernameValidator _usernameValidator;
+ private readonly UsernameValidator _usernameValidator = new UsernameValidator();
- public UserService(ILogger<UserService> logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService)
+ public UserService(ILogger<UserService> logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IPasswordService passwordService)
{
_logger = logger;
_memoryCache = memoryCache;
_databaseContext = databaseContext;
- _jwtService = jwtService;
_passwordService = passwordService;
-
- _usernameValidator = new UsernameValidator();
}
private static string GenerateCacheKeyByUserId(long id) => $"user:{id}";
@@ -176,12 +151,13 @@ namespace Timeline.Services
}
}
- public async Task<CreateTokenResult> CreateToken(string username, string password, DateTime? expires)
+ public async Task<UserInfo> CheckCredential(string username, string password)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
+
CheckUsernameFormat(username);
// We need password info, so always check the database.
@@ -231,12 +207,12 @@ namespace Timeline.Services
}
if (tokenInfo.Version != cache.Version)
- throw new JwtVerifyException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtVerifyException.ErrorCodes.OldVersion);
+ throw new JwtUserTokenBadFormatException(new JwtBadVersionException(tokenInfo.Version, cache.Version), JwtUserTokenBadFormatException.ErrorCodes.OldVersion);
return cache.ToUserInfo();
}
- public async Task<UserInfo> GetUser(string username)
+ public async Task<UserInfo> GetUserByUsername(string username)
{
if (username == null)
throw new ArgumentNullException(nameof(username));
@@ -267,7 +243,7 @@ namespace Timeline.Services
if (user == null)
{
- var newUser = new User
+ var newUser = new UserEntity
{
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
diff --git a/Timeline/Services/UserTokenException.cs b/Timeline/Services/UserTokenException.cs
new file mode 100644
index 00000000..e63305b1
--- /dev/null
+++ b/Timeline/Services/UserTokenException.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Timeline.Services
+{
+
+ [Serializable]
+ public class UserTokenException : Exception
+ {
+ public UserTokenException() { }
+ public UserTokenException(string message) : base(message) { }
+ public UserTokenException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenException(string token, string message) : base(message) { Token = token; }
+ public UserTokenException(string token, string message, Exception inner) : base(message, inner) { Token = token; }
+ protected UserTokenException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string Token { get; private set; } = "";
+ }
+
+
+ [Serializable]
+ public class UserTokenTimeExpireException : UserTokenException
+ {
+ public UserTokenTimeExpireException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { }
+ public UserTokenTimeExpireException(string message) : base(message) { }
+ public UserTokenTimeExpireException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; }
+ public UserTokenTimeExpireException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; }
+ protected UserTokenTimeExpireException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public DateTime ExpireTime { get; private set; } = default;
+
+ public DateTime VerifyTime { get; private set; } = default;
+ }
+
+ [Serializable]
+ public class UserTokenBadVersionException : UserTokenException
+ {
+ public UserTokenBadVersionException() : base(Resources.Services.Exception.UserTokenBadVersionException) { }
+ public UserTokenBadVersionException(string message) : base(message) { }
+ public UserTokenBadVersionException(string message, Exception inner) : base(message, inner) { }
+ public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; }
+ public UserTokenBadVersionException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; }
+ protected UserTokenBadVersionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public long TokenVersion { get; set; }
+
+ public long RequiredVersion { get; set; }
+ }
+
+ [Serializable]
+ public class UserTokenBadFormatException : UserTokenException
+ {
+ public UserTokenBadFormatException() : base(Resources.Services.Exception.UserTokenBadFormatException) { }
+ public UserTokenBadFormatException(string token) : base(token, Resources.Services.Exception.UserTokenBadFormatException) { }
+ public UserTokenBadFormatException(string token, string message) : base(token, message) { }
+ public UserTokenBadFormatException(string token, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadFormatException, inner) { }
+ public UserTokenBadFormatException(string token, string message, Exception inner) : base(token, message, inner) { }
+ protected UserTokenBadFormatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/Timeline/Services/UserTokenManager.cs b/Timeline/Services/UserTokenManager.cs
new file mode 100644
index 00000000..c3cb51c9
--- /dev/null
+++ b/Timeline/Services/UserTokenManager.cs
@@ -0,0 +1,93 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+using Timeline.Models;
+
+namespace Timeline.Services
+{
+ public class UserTokenCreateResult
+ {
+ public string Token { get; set; } = default!;
+ public UserInfo User { get; set; } = default!;
+ }
+
+ public interface IUserTokenManager
+ {
+ /// <summary>
+ /// Try to create a token for given username and password.
+ /// </summary>
+ /// <param name="username">The username.</param>
+ /// <param name="password">The password.</param>
+ /// <param name="expireAt">The expire time of the token.</param>
+ /// <returns>The created token and the user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown when <paramref name="username"/> is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with <paramref name="username"/> does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when <paramref name="password"/> is wrong.</exception>
+ public Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null);
+
+ /// <summary>
+ /// Verify a token and get the saved user info. This also check the database for existence of the user.
+ /// </summary>
+ /// <param name="token">The token.</param>
+ /// <returns>The user stored in token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="UserTokenTimeExpireException">Thrown when the token is expired.</exception>
+ /// <exception cref="UserTokenBadVersionException">Thrown when the token is of bad version.</exception>
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued.</exception>
+ public Task<UserInfo> VerifyToken(string token);
+ }
+
+ public class UserTokenManager : IUserTokenManager
+ {
+ private readonly ILogger<UserTokenManager> _logger;
+ private readonly IUserService _userService;
+ private readonly IUserTokenService _userTokenService;
+ private readonly IClock _clock;
+
+ public UserTokenManager(ILogger<UserTokenManager> logger, IUserService userService, IUserTokenService userTokenService, IClock clock)
+ {
+ _logger = logger;
+ _userService = userService;
+ _userTokenService = userTokenService;
+ _clock = clock;
+ }
+
+ public async Task<UserTokenCreateResult> CreateToken(string username, string password, DateTime? expireAt = null)
+ {
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ var user = await _userService.VerifyCredential(username, password);
+ var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt });
+
+ return new UserTokenCreateResult { Token = token, User = user };
+ }
+
+
+ public async Task<UserInfo> VerifyToken(string token)
+ {
+ if (token == null)
+ throw new ArgumentNullException(nameof(token));
+
+ var tokenInfo = _userTokenService.VerifyToken(token);
+
+ if (tokenInfo.ExpireAt.HasValue)
+ {
+ var currentTime = _clock.GetCurrentTime();
+ if (tokenInfo.ExpireAt < currentTime)
+ throw new UserTokenTimeExpireException(token, tokenInfo.ExpireAt.Value, currentTime);
+ }
+
+ var user = await _userService.GetUserById(tokenInfo.Id);
+
+ if (tokenInfo.Version < user.Version)
+ throw new UserTokenBadVersionException(token, tokenInfo.Version, user.Version);
+
+ return user;
+ }
+ }
+}
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/UserTokenService.cs
index bf92966a..c246fdff 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/UserTokenService.cs
@@ -9,50 +9,60 @@ using Timeline.Configs;
namespace Timeline.Services
{
- public class TokenInfo
+ public class UserTokenInfo
{
public long Id { get; set; }
public long Version { get; set; }
+ public DateTime? ExpireAt { get; set; }
}
- public interface IJwtService
+ public interface IUserTokenService
{
/// <summary>
- /// Create a JWT token for a given token info.
+ /// Create a 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);
+ string GenerateToken(UserTokenInfo tokenInfo);
/// <summary>
- /// Verify a JWT token.
- /// Return null is <paramref name="token"/> is null.
+ /// Verify a token and get the saved info.
/// </summary>
- /// <param name="token">The token string to verify.</param>
- /// <returns>Return the saved info in token.</returns>
+ /// <param name="token">The token to verify.</param>
+ /// <returns>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);
-
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <remarks>
+ /// If this method throw <see cref="UserTokenBadFormatException"/>, it usually means the token is not created by this service.
+ /// </remarks>
+ UserTokenInfo VerifyToken(string token);
}
- public class JwtService : IJwtService
+ public class JwtUserTokenService : IUserTokenService
{
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)
+ private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
+ private SymmetricSecurityKey _tokenSecurityKey;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "<Pending>")]
+ public JwtUserTokenService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
{
_jwtConfig = jwtConfig;
_clock = clock;
+
+ _tokenSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.CurrentValue.SigningKey));
+ jwtConfig.OnChange(config =>
+ {
+ _tokenSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey));
+ });
}
- public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null)
+ public string GenerateToken(UserTokenInfo tokenInfo)
{
if (tokenInfo == null)
throw new ArgumentNullException(nameof(tokenInfo));
@@ -71,7 +81,7 @@ namespace Timeline.Services
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384),
IssuedAt = _clock.GetCurrentTime(),
- Expires = expires.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)),
+ Expires = tokenInfo.ExpireAt.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.
};
@@ -82,7 +92,7 @@ namespace Timeline.Services
}
- public TokenInfo VerifyJwtToken(string token)
+ public UserTokenInfo VerifyToken(string token)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
@@ -95,37 +105,42 @@ namespace Timeline.Services
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
- ValidateLifetime = true,
+ ValidateLifetime = false,
ValidIssuer = config.Issuer,
ValidAudience = config.Audience,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
- }, out _);
+ IssuerSigningKey = _tokenSecurityKey
+ }, out var t);
var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (idClaim == null)
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoIdClaim);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim);
if (!long.TryParse(idClaim, out var id))
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat);
var versionClaim = principal.FindFirstValue(VersionClaimType);
if (versionClaim == null)
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.NoVersionClaim);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim);
if (!long.TryParse(versionClaim, out var version))
- throw new JwtVerifyException(JwtVerifyException.ErrorCodes.VersionClaimBadFormat);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat);
+
+ var decodedToken = (JwtSecurityToken)t;
+ var exp = decodedToken.Payload.Exp;
+ DateTime? expireAt = null;
+ if (exp.HasValue)
+ {
+ expireAt = EpochTime.DateTime(exp.Value);
+ }
- return new TokenInfo
+ return new UserTokenInfo
{
Id = id,
- Version = version
+ Version = version,
+ ExpireAt = expireAt
};
}
- catch (SecurityTokenExpiredException e)
- {
- throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Expired);
- }
- catch (Exception e)
+ catch (Exception e) when (e is SecurityTokenException || e is ArgumentException)
{
- throw new JwtVerifyException(e, JwtVerifyException.ErrorCodes.Others);
+ throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e);
}
}
}
diff --git a/Timeline/Services/UsernameBadFormatException.cs b/Timeline/Services/UsernameBadFormatException.cs
index d82bf962..991be7df 100644
--- a/Timeline/Services/UsernameBadFormatException.cs
+++ b/Timeline/Services/UsernameBadFormatException.cs
@@ -22,6 +22,6 @@ namespace Timeline.Services
/// <summary>
/// Username of bad format.
/// </summary>
- public string? Username { get; private set; }
+ public string Username { get; private set; } = "";
}
}