From 7c37a5885437aaf97b9986b7cc2941b5e4316003 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 24 Apr 2021 22:01:27 +0800 Subject: refactor: Move token services. --- BackEnd/Timeline/Auth/MyAuthenticationHandler.cs | 1 + BackEnd/Timeline/Configs/JwtConfiguration.cs | 14 -- BackEnd/Timeline/Configs/JwtOptions.cs | 8 ++ BackEnd/Timeline/Configs/TokenOptions.cs | 11 ++ BackEnd/Timeline/Controllers/TokenController.cs | 1 + .../Services/JwtUserTokenBadFormatException.cs | 48 ------- .../Token/JwtUserTokenBadFormatException.cs | 49 +++++++ .../Token/TokenServiceColletionExtensions.cs | 18 +++ .../Timeline/Services/Token/UserTokenException.cs | 83 ++++++++++++ .../Timeline/Services/Token/UserTokenHandler.cs | 146 ++++++++++++++++++++ .../Timeline/Services/Token/UserTokenManager.cs | 115 ++++++++++++++++ BackEnd/Timeline/Services/UserTokenException.cs | 83 ------------ BackEnd/Timeline/Services/UserTokenHandler.cs | 149 --------------------- BackEnd/Timeline/Services/UserTokenManager.cs | 108 --------------- BackEnd/Timeline/Startup.cs | 6 +- BackEnd/Timeline/appsettings.json | 4 +- 16 files changed, 437 insertions(+), 407 deletions(-) delete mode 100644 BackEnd/Timeline/Configs/JwtConfiguration.cs create mode 100644 BackEnd/Timeline/Configs/JwtOptions.cs create mode 100644 BackEnd/Timeline/Configs/TokenOptions.cs delete mode 100644 BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs create mode 100644 BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs create mode 100644 BackEnd/Timeline/Services/Token/TokenServiceColletionExtensions.cs create mode 100644 BackEnd/Timeline/Services/Token/UserTokenException.cs create mode 100644 BackEnd/Timeline/Services/Token/UserTokenHandler.cs create mode 100644 BackEnd/Timeline/Services/Token/UserTokenManager.cs delete mode 100644 BackEnd/Timeline/Services/UserTokenException.cs delete mode 100644 BackEnd/Timeline/Services/UserTokenHandler.cs delete mode 100644 BackEnd/Timeline/Services/UserTokenManager.cs (limited to 'BackEnd/Timeline') diff --git a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs index f3d18a0e..fe27814a 100644 --- a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs +++ b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Timeline.Models; using Timeline.Models.Http; using Timeline.Services; +using Timeline.Services.Token; namespace Timeline.Auth { diff --git a/BackEnd/Timeline/Configs/JwtConfiguration.cs b/BackEnd/Timeline/Configs/JwtConfiguration.cs deleted file mode 100644 index af8052de..00000000 --- a/BackEnd/Timeline/Configs/JwtConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Timeline.Configs -{ - public class JwtConfiguration - { - public string Issuer { get; set; } = default!; - public string Audience { get; set; } = default!; - - /// - /// Set the default value of expire offset of jwt token. - /// Unit is second. Default is 3600 * 24 seconds, aka 1 day. - /// - public long DefaultExpireOffset { get; set; } = 3600 * 24; - } -} diff --git a/BackEnd/Timeline/Configs/JwtOptions.cs b/BackEnd/Timeline/Configs/JwtOptions.cs new file mode 100644 index 00000000..c400b8a6 --- /dev/null +++ b/BackEnd/Timeline/Configs/JwtOptions.cs @@ -0,0 +1,8 @@ +namespace Timeline.Configs +{ + public class JwtOptions + { + public string Issuer { get; set; } = default!; + public string Audience { get; set; } = default!; + } +} diff --git a/BackEnd/Timeline/Configs/TokenOptions.cs b/BackEnd/Timeline/Configs/TokenOptions.cs new file mode 100644 index 00000000..e7d4d9e7 --- /dev/null +++ b/BackEnd/Timeline/Configs/TokenOptions.cs @@ -0,0 +1,11 @@ +namespace Timeline.Configs +{ + public class TokenOptions + { + /// + /// Set the default value of expire offset of jwt token. + /// Unit is second. Default is 3600 * 24 seconds, aka 1 day. + /// + public long DefaultExpireSeconds { get; set; } = 3600 * 24; + } +} diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 3ff8acf5..7df3891c 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -10,6 +10,7 @@ using Timeline.Models.Http; using Timeline.Models.Mapper; using Timeline.Services; using Timeline.Services.Exceptions; +using Timeline.Services.Token; using static Timeline.Resources.Controllers.TokenController; namespace Timeline.Controllers diff --git a/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs b/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs deleted file mode 100644 index c528c3e3..00000000 --- a/BackEnd/Timeline/Services/JwtUserTokenBadFormatException.cs +++ /dev/null @@ -1,48 +0,0 @@ -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/BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs b/BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs new file mode 100644 index 00000000..4d7300a1 --- /dev/null +++ b/BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; +using static Timeline.Resources.Services.Exception; + +namespace Timeline.Services.Token +{ + [Serializable] + public class JwtUserTokenBadFormatException : UserTokenBadFormatException + { + public enum ErrorKind + { + NoIdClaim, + IdClaimBadFormat, + NoVersionClaim, + VersionClaimBadFormat, + NoExp, + 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/BackEnd/Timeline/Services/Token/TokenServiceColletionExtensions.cs b/BackEnd/Timeline/Services/Token/TokenServiceColletionExtensions.cs new file mode 100644 index 00000000..d3219ec4 --- /dev/null +++ b/BackEnd/Timeline/Services/Token/TokenServiceColletionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Timeline.Configs; + +namespace Timeline.Services.Token +{ + public static class TokenServiceColletionExtensions + { + public static IServiceCollection AddTokenService(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("Token")); + services.Configure(configuration.GetSection("Jwt")); + services.AddScoped(); + services.AddScoped(); + return services; + } + } +} diff --git a/BackEnd/Timeline/Services/Token/UserTokenException.cs b/BackEnd/Timeline/Services/Token/UserTokenException.cs new file mode 100644 index 00000000..d666ba10 --- /dev/null +++ b/BackEnd/Timeline/Services/Token/UserTokenException.cs @@ -0,0 +1,83 @@ +using System; + +namespace Timeline.Services.Token +{ + + [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 UserTokenTimeExpiredException : UserTokenException + { + public UserTokenTimeExpiredException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { } + public UserTokenTimeExpiredException(string message) : base(message) { } + public UserTokenTimeExpiredException(string message, Exception inner) : base(message, inner) { } + public UserTokenTimeExpiredException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; } + public UserTokenTimeExpiredException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; } + protected UserTokenTimeExpiredException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public DateTime ExpireTime { get; private set; } + + public DateTime VerifyTime { get; private set; } + } + + [Serializable] + public class UserTokenVersionExpiredException : UserTokenException + { + public UserTokenVersionExpiredException() : base(Resources.Services.Exception.UserTokenBadVersionException) { } + public UserTokenVersionExpiredException(string message) : base(message) { } + public UserTokenVersionExpiredException(string message, Exception inner) : base(message, inner) { } + public UserTokenVersionExpiredException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + public UserTokenVersionExpiredException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } + protected UserTokenVersionExpiredException( + 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 UserTokenUserNotExistException : UserTokenException + { + const string message = "The owner of the token does not exist."; + + public UserTokenUserNotExistException() : base(message) { } + public UserTokenUserNotExistException(string token) : base(token, message) { } + public UserTokenUserNotExistException(string token, Exception inner) : base(token, message, inner) { } + + protected UserTokenUserNotExistException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + + [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/BackEnd/Timeline/Services/Token/UserTokenHandler.cs b/BackEnd/Timeline/Services/Token/UserTokenHandler.cs new file mode 100644 index 00000000..2eaea57e --- /dev/null +++ b/BackEnd/Timeline/Services/Token/UserTokenHandler.cs @@ -0,0 +1,146 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using Timeline.Configs; +using Timeline.Entities; + +namespace Timeline.Services.Token +{ + public class UserTokenInfo + { + public long Id { get; set; } + public long Version { get; set; } + public DateTime ExpireAt { get; set; } + } + + public interface IUserTokenHandler + { + /// + /// Create a token for a given token info. + /// + /// The info to generate token. + /// Return the generated token. + /// Thrown when is null. + string GenerateToken(UserTokenInfo tokenInfo); + + /// + /// Verify a token and get the saved info. Do not validate lifetime!!! + /// + /// The token to verify. + /// The saved info in token. + /// Thrown when is null. + /// Thrown when the token is of bad format. + /// + /// If this method throw , it usually means the token is not created by this service. + /// + UserTokenInfo VerifyToken(string token); + } + + public class JwtUserTokenHandler : IUserTokenHandler + { + private const string VersionClaimType = "timeline_version"; + + private readonly IOptionsMonitor _jwtConfig; + private readonly IClock _clock; + + private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); + private SymmetricSecurityKey _tokenSecurityKey; + + public JwtUserTokenHandler(IOptionsMonitor jwtConfig, IClock clock, DatabaseContext database) + { + _jwtConfig = jwtConfig; + _clock = clock; + + var key = database.JwtToken.Select(t => t.Key).SingleOrDefault(); + + if (key == null) + { + throw new InvalidOperationException(Resources.Services.UserTokenService.JwtKeyNotExist); + } + + _tokenSecurityKey = new SymmetricSecurityKey(key); + } + + public string GenerateToken(UserTokenInfo tokenInfo) + { + 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(_tokenSecurityKey, SecurityAlgorithms.HmacSha384), + IssuedAt = _clock.GetCurrentTime(), + Expires = tokenInfo.ExpireAt, + 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 UserTokenInfo VerifyToken(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 = false, + ValidIssuer = config.Issuer, + ValidAudience = config.Audience, + IssuerSigningKey = _tokenSecurityKey + }, out var t); + + var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (idClaim == null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim); + if (!long.TryParse(idClaim, out var id)) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat); + + var versionClaim = principal.FindFirstValue(VersionClaimType); + if (versionClaim == null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim); + if (!long.TryParse(versionClaim, out var version)) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.VersionClaimBadFormat); + + var decodedToken = (JwtSecurityToken)t; + var exp = decodedToken.Payload.Exp; + if (exp is null) + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoExp); + + return new UserTokenInfo + { + Id = id, + Version = version, + ExpireAt = EpochTime.DateTime(exp.Value) + }; + } + catch (Exception e) when (e is SecurityTokenException || e is ArgumentException) + { + throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e); + } + } + } +} diff --git a/BackEnd/Timeline/Services/Token/UserTokenManager.cs b/BackEnd/Timeline/Services/Token/UserTokenManager.cs new file mode 100644 index 00000000..00bc2cf7 --- /dev/null +++ b/BackEnd/Timeline/Services/Token/UserTokenManager.cs @@ -0,0 +1,115 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; +using Timeline.Configs; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Services.Exceptions; + +namespace Timeline.Services.Token +{ + public class UserTokenCreateResult + { + public string Token { get; set; } = default!; + public UserEntity User { get; set; } = default!; + } + + public interface IUserTokenManager + { + /// + /// Try to create a token for given username and password. + /// + /// The username. + /// The password. + /// The expire time of the token. + /// The created token and the user info. + /// Thrown when or is null. + /// Thrown when is of bad format. + /// Thrown when the user with does not exist. + /// Thrown when is wrong. + public Task CreateToken(string username, string password, DateTime? expireAt = null); + + /// + /// Verify a token and get the saved user info. This also check the database for existence of the user. + /// + /// The token. + /// The user stored in token. + /// Thrown when is null. + /// Thrown when the token is expired. + /// Thrown when the token is of bad version. + /// Thrown when the token is of bad format. + /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. + public Task VerifyToken(string token); + } + + public class UserTokenManager : IUserTokenManager + { + private readonly ILogger _logger; + private readonly IOptionsMonitor _tokenOptionsMonitor; + private readonly IUserService _userService; + private readonly IUserCredentialService _userCredentialService; + private readonly IUserTokenHandler _userTokenService; + private readonly IClock _clock; + + public UserTokenManager(ILogger logger, IOptionsMonitor tokenOptionsMonitor, IUserService userService, IUserCredentialService userCredentialService, IUserTokenHandler userTokenService, IClock clock) + { + _logger = logger; + _tokenOptionsMonitor = tokenOptionsMonitor; + _userService = userService; + _userCredentialService = userCredentialService; + _userTokenService = userTokenService; + _clock = clock; + } + + public async Task CreateToken(string username, string password, DateTime? expireAt = null) + { + expireAt = expireAt?.MyToUtc(); + + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + + var userId = await _userCredentialService.VerifyCredential(username, password); + var user = await _userService.GetUser(userId); + + var token = _userTokenService.GenerateToken(new UserTokenInfo + { + Id = user.Id, + Version = user.Version, + ExpireAt = expireAt ?? _clock.GetCurrentTime() + TimeSpan.FromSeconds(_tokenOptionsMonitor.CurrentValue.DefaultExpireSeconds) + }); + + return new UserTokenCreateResult { Token = token, User = user }; + } + + + public async Task VerifyToken(string token) + { + if (token == null) + throw new ArgumentNullException(nameof(token)); + + var tokenInfo = _userTokenService.VerifyToken(token); + + var currentTime = _clock.GetCurrentTime(); + if (tokenInfo.ExpireAt < currentTime) + throw new UserTokenTimeExpiredException(token, tokenInfo.ExpireAt, currentTime); + + try + { + var user = await _userService.GetUser(tokenInfo.Id); + + if (tokenInfo.Version < user.Version) + throw new UserTokenVersionExpiredException(token, tokenInfo.Version, user.Version); + + return user; + + } + catch (UserNotExistException e) + { + throw new UserTokenUserNotExistException(token, e); + } + } + } +} diff --git a/BackEnd/Timeline/Services/UserTokenException.cs b/BackEnd/Timeline/Services/UserTokenException.cs deleted file mode 100644 index 398da41f..00000000 --- a/BackEnd/Timeline/Services/UserTokenException.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; - -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 UserTokenTimeExpiredException : UserTokenException - { - public UserTokenTimeExpiredException() : base(Resources.Services.Exception.UserTokenTimeExpireException) { } - public UserTokenTimeExpiredException(string message) : base(message) { } - public UserTokenTimeExpiredException(string message, Exception inner) : base(message, inner) { } - public UserTokenTimeExpiredException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resources.Services.Exception.UserTokenTimeExpireException) { ExpireTime = expireTime; VerifyTime = verifyTime; } - public UserTokenTimeExpiredException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resources.Services.Exception.UserTokenTimeExpireException, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; } - protected UserTokenTimeExpiredException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public DateTime ExpireTime { get; private set; } - - public DateTime VerifyTime { get; private set; } - } - - [Serializable] - public class UserTokenVersionExpiredException : UserTokenException - { - public UserTokenVersionExpiredException() : base(Resources.Services.Exception.UserTokenBadVersionException) { } - public UserTokenVersionExpiredException(string message) : base(message) { } - public UserTokenVersionExpiredException(string message, Exception inner) : base(message, inner) { } - public UserTokenVersionExpiredException(string token, long tokenVersion, long requiredVersion) : base(token, Resources.Services.Exception.UserTokenBadVersionException) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } - public UserTokenVersionExpiredException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resources.Services.Exception.UserTokenBadVersionException, inner) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } - protected UserTokenVersionExpiredException( - 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 UserTokenUserNotExistException : UserTokenException - { - const string message = "The owner of the token does not exist."; - - public UserTokenUserNotExistException() : base(message) { } - public UserTokenUserNotExistException(string token) : base(token, message) { } - public UserTokenUserNotExistException(string token, Exception inner) : base(token, message, inner) { } - - protected UserTokenUserNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } - - [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/BackEnd/Timeline/Services/UserTokenHandler.cs b/BackEnd/Timeline/Services/UserTokenHandler.cs deleted file mode 100644 index c24a8d47..00000000 --- a/BackEnd/Timeline/Services/UserTokenHandler.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using System; -using System.Globalization; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; -using Timeline.Configs; -using Timeline.Entities; - -namespace Timeline.Services -{ - public class UserTokenInfo - { - public long Id { get; set; } - public long Version { get; set; } - public DateTime? ExpireAt { get; set; } - } - - public interface IUserTokenHandler - { - /// - /// Create a token for a given token info. - /// - /// The info to generate token. - /// Return the generated token. - /// Thrown when is null. - string GenerateToken(UserTokenInfo tokenInfo); - - /// - /// Verify a token and get the saved info. - /// - /// The token to verify. - /// The saved info in token. - /// Thrown when is null. - /// Thrown when the token is of bad format. - /// - /// If this method throw , it usually means the token is not created by this service. - /// - UserTokenInfo VerifyToken(string token); - } - - public class JwtUserTokenHandler : IUserTokenHandler - { - private const string VersionClaimType = "timeline_version"; - - private readonly IOptionsMonitor _jwtConfig; - private readonly IClock _clock; - - private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); - private SymmetricSecurityKey _tokenSecurityKey; - - public JwtUserTokenHandler(IOptionsMonitor jwtConfig, IClock clock, DatabaseContext database) - { - _jwtConfig = jwtConfig; - _clock = clock; - - var key = database.JwtToken.Select(t => t.Key).SingleOrDefault(); - - if (key == null) - { - throw new InvalidOperationException(Resources.Services.UserTokenService.JwtKeyNotExist); - } - - _tokenSecurityKey = new SymmetricSecurityKey(key); - } - - public string GenerateToken(UserTokenInfo tokenInfo) - { - 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(_tokenSecurityKey, SecurityAlgorithms.HmacSha384), - IssuedAt = _clock.GetCurrentTime(), - 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. - }; - - var token = _tokenHandler.CreateToken(tokenDescriptor); - var tokenString = _tokenHandler.WriteToken(token); - - return tokenString; - } - - - public UserTokenInfo VerifyToken(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 = false, - ValidIssuer = config.Issuer, - ValidAudience = config.Audience, - IssuerSigningKey = _tokenSecurityKey - }, out var t); - - var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier); - if (idClaim == null) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoIdClaim); - if (!long.TryParse(idClaim, out var id)) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.IdClaimBadFormat); - - var versionClaim = principal.FindFirstValue(VersionClaimType); - if (versionClaim == null) - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.NoVersionClaim); - if (!long.TryParse(versionClaim, out var version)) - 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 UserTokenInfo - { - Id = id, - Version = version, - ExpireAt = expireAt - }; - } - catch (Exception e) when (e is SecurityTokenException || e is ArgumentException) - { - throw new JwtUserTokenBadFormatException(token, JwtUserTokenBadFormatException.ErrorKind.Other, e); - } - } - } -} diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs deleted file mode 100644 index 898e4d6d..00000000 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Helpers; -using Timeline.Services.Exceptions; - -namespace Timeline.Services -{ - public class UserTokenCreateResult - { - public string Token { get; set; } = default!; - public UserEntity User { get; set; } = default!; - } - - public interface IUserTokenManager - { - /// - /// Try to create a token for given username and password. - /// - /// The username. - /// The password. - /// The expire time of the token. - /// The created token and the user info. - /// Thrown when or is null. - /// Thrown when is of bad format. - /// Thrown when the user with does not exist. - /// Thrown when is wrong. - public Task CreateToken(string username, string password, DateTime? expireAt = null); - - /// - /// Verify a token and get the saved user info. This also check the database for existence of the user. - /// - /// The token. - /// The user stored in token. - /// Thrown when is null. - /// Thrown when the token is expired. - /// Thrown when the token is of bad version. - /// Thrown when the token is of bad format. - /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); - } - - public class UserTokenManager : IUserTokenManager - { - private readonly ILogger _logger; - private readonly IUserService _userService; - private readonly IUserCredentialService _userCredentialService; - private readonly IUserTokenHandler _userTokenService; - private readonly IClock _clock; - - public UserTokenManager(ILogger logger, IUserService userService, IUserCredentialService userCredentialService, IUserTokenHandler userTokenService, IClock clock) - { - _logger = logger; - _userService = userService; - _userCredentialService = userCredentialService; - _userTokenService = userTokenService; - _clock = clock; - } - - public async Task CreateToken(string username, string password, DateTime? expireAt = null) - { - expireAt = expireAt?.MyToUtc(); - - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - var userId = await _userCredentialService.VerifyCredential(username, password); - var user = await _userService.GetUser(userId); - var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); - - return new UserTokenCreateResult { Token = token, User = user }; - } - - - public async Task 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 UserTokenTimeExpiredException(token, tokenInfo.ExpireAt.Value, currentTime); - } - - try - { - var user = await _userService.GetUser(tokenInfo.Id); - - if (tokenInfo.Version < user.Version) - throw new UserTokenVersionExpiredException(token, tokenInfo.Version, user.Version); - - return user; - - } - catch (UserNotExistException e) - { - throw new UserTokenUserNotExistException(token, e); - } - } - } -} diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index c2134a94..d4fffb51 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -22,6 +22,7 @@ using Timeline.Models.Mapper; using Timeline.Routes; using Timeline.Services; using Timeline.Services.DatabaseManagement; +using Timeline.Services.Token; using Timeline.Swagger; namespace Timeline @@ -78,7 +79,6 @@ namespace Timeline options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory; }); - services.Configure(Configuration.GetSection("Jwt")); services.AddAuthentication(AuthenticationConstants.Scheme) .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); services.AddAuthorization(); @@ -111,11 +111,11 @@ namespace Timeline services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddUserAvatarService(); + services.AddTokenService(Configuration); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/BackEnd/Timeline/appsettings.json b/BackEnd/Timeline/appsettings.json index 804ca43a..5098b4ae 100644 --- a/BackEnd/Timeline/appsettings.json +++ b/BackEnd/Timeline/appsettings.json @@ -5,7 +5,7 @@ } }, "Jwt": { - "Issuer": "api.crupest.xyz", - "Audience": "api.crupest.xyz" + "Issuer": "timeline.crupest.life", + "Audience": "timeline.crupest.life" } } -- cgit v1.2.3