aboutsummaryrefslogtreecommitdiff
path: root/BackEnd/Timeline/Services/Token
diff options
context:
space:
mode:
Diffstat (limited to 'BackEnd/Timeline/Services/Token')
-rw-r--r--BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs49
-rw-r--r--BackEnd/Timeline/Services/Token/TokenServiceColletionExtensions.cs18
-rw-r--r--BackEnd/Timeline/Services/Token/UserTokenException.cs83
-rw-r--r--BackEnd/Timeline/Services/Token/UserTokenHandler.cs146
-rw-r--r--BackEnd/Timeline/Services/Token/UserTokenManager.cs115
5 files changed, 411 insertions, 0 deletions
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<TokenOptions>(configuration.GetSection("Token"));
+ services.Configure<JwtOptions>(configuration.GetSection("Jwt"));
+ services.AddScoped<IUserTokenHandler, JwtUserTokenHandler>();
+ services.AddScoped<IUserTokenManager, UserTokenManager>();
+ 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
+ {
+ /// <summary>
+ /// Create a token for a given token info.
+ /// </summary>
+ /// <param name="tokenInfo">The info to generate token.</param>
+ /// <returns>Return the generated token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
+ string GenerateToken(UserTokenInfo tokenInfo);
+
+ /// <summary>
+ /// Verify a token and get the saved info. Do not validate lifetime!!!
+ /// </summary>
+ /// <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="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 JwtUserTokenHandler : IUserTokenHandler
+ {
+ private const string VersionClaimType = "timeline_version";
+
+ private readonly IOptionsMonitor<JwtOptions> _jwtConfig;
+ private readonly IClock _clock;
+
+ private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
+ private SymmetricSecurityKey _tokenSecurityKey;
+
+ public JwtUserTokenHandler(IOptionsMonitor<JwtOptions> 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
+ {
+ /// <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="ArgumentException">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="UserTokenTimeExpiredException">Thrown when the token is expired.</exception>
+ /// <exception cref="UserTokenVersionExpiredException">Thrown when the token is of bad version.</exception>
+ /// <exception cref="UserTokenBadFormatException">Thrown when the token is of bad format.</exception>
+ /// <exception cref="UserTokenUserNotExistException">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<UserEntity> VerifyToken(string token);
+ }
+
+ public class UserTokenManager : IUserTokenManager
+ {
+ private readonly ILogger<UserTokenManager> _logger;
+ private readonly IOptionsMonitor<TokenOptions> _tokenOptionsMonitor;
+ private readonly IUserService _userService;
+ private readonly IUserCredentialService _userCredentialService;
+ private readonly IUserTokenHandler _userTokenService;
+ private readonly IClock _clock;
+
+ public UserTokenManager(ILogger<UserTokenManager> logger, IOptionsMonitor<TokenOptions> tokenOptionsMonitor, IUserService userService, IUserCredentialService userCredentialService, IUserTokenHandler userTokenService, IClock clock)
+ {
+ _logger = logger;
+ _tokenOptionsMonitor = tokenOptionsMonitor;
+ _userService = userService;
+ _userCredentialService = userCredentialService;
+ _userTokenService = userTokenService;
+ _clock = clock;
+ }
+
+ public async Task<UserTokenCreateResult> 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<UserEntity> 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);
+ }
+ }
+ }
+}