diff options
author | crupest <crupest@outlook.com> | 2020-01-21 01:11:17 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-01-21 01:11:17 +0800 |
commit | 4aadb05cd5718c7d16bf432c96e23ae4e7db4783 (patch) | |
tree | a45506852659b9d8e2bfe0b9e58a496060f7cd9b | |
parent | e6069a6980ec6d2505e19026d4c84a9588f153dc (diff) | |
download | timeline-4aadb05cd5718c7d16bf432c96e23ae4e7db4783.tar.gz timeline-4aadb05cd5718c7d16bf432c96e23ae4e7db4783.tar.bz2 timeline-4aadb05cd5718c7d16bf432c96e23ae4e7db4783.zip |
...
33 files changed, 408 insertions, 298 deletions
diff --git a/Timeline.Tests/Controllers/TokenControllerTest.cs b/Timeline.Tests/Controllers/TokenControllerTest.cs index 2b3547ea..740d8377 100644 --- a/Timeline.Tests/Controllers/TokenControllerTest.cs +++ b/Timeline.Tests/Controllers/TokenControllerTest.cs @@ -97,9 +97,9 @@ namespace Timeline.Tests.Controllers public static IEnumerable<object[]> Verify_BadRequest_Data()
{
- yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.Expired), ErrorCodes.TokenController.Verify_TimeExpired };
- yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.IdClaimBadFormat), ErrorCodes.TokenController.Verify_BadFormat };
- yield return new object[] { new JwtVerifyException(JwtVerifyException.ErrorCodes.OldVersion), ErrorCodes.TokenController.Verify_OldVersion };
+ yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.Expired), ErrorCodes.TokenController.Verify_TimeExpired };
+ yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.IdClaimBadFormat), ErrorCodes.TokenController.Verify_BadFormat };
+ yield return new object[] { new JwtUserTokenBadFormatException(JwtUserTokenBadFormatException.ErrorCodes.OldVersion), ErrorCodes.TokenController.Verify_OldVersion };
yield return new object[] { new UserNotExistException(), ErrorCodes.TokenController.Verify_UserNotExist };
}
diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index 043062c3..262dbe11 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -46,7 +46,7 @@ namespace Timeline.Tests.Controllers public async Task Get_Success()
{
const string username = "aaa";
- _mockUserService.Setup(s => s.GetUser(username)).ReturnsAsync(MockUser.User.Info);
+ _mockUserService.Setup(s => s.GetUserByUsername(username)).ReturnsAsync(MockUser.User.Info);
var action = await _controller.Get(username);
action.Result.Should().BeAssignableTo<OkObjectResult>()
.Which.Value.Should().BeEquivalentTo(MockUser.User.Info);
@@ -56,7 +56,7 @@ namespace Timeline.Tests.Controllers public async Task Get_NotFound()
{
const string username = "aaa";
- _mockUserService.Setup(s => s.GetUser(username)).Returns(Task.FromResult<UserInfo>(null));
+ _mockUserService.Setup(s => s.GetUserByUsername(username)).Returns(Task.FromResult<User>(null));
var action = await _controller.Get(username);
action.Result.Should().BeAssignableTo<NotFoundObjectResult>()
.Which.Value.Should().BeAssignableTo<CommonResponse>()
diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index a7b97c16..a15823a9 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -28,7 +28,7 @@ namespace Timeline.Tests {
var user = _context.Users.First();
_context.UserAvatars.Count().Should().Be(0);
- _context.UserAvatars.Add(new UserAvatar
+ _context.UserAvatars.Add(new UserAvatarEntity
{
Data = null,
Type = null,
@@ -48,7 +48,7 @@ namespace Timeline.Tests {
var user = _context.Users.First();
_context.UserDetails.Count().Should().Be(0);
- _context.UserDetails.Add(new UserDetail
+ _context.UserDetails.Add(new UserDetailEntity
{
Nickname = null,
UserId = user.Id
diff --git a/Timeline.Tests/Helpers/TestDatabase.cs b/Timeline.Tests/Helpers/TestDatabase.cs index 9560f353..3163279a 100644 --- a/Timeline.Tests/Helpers/TestDatabase.cs +++ b/Timeline.Tests/Helpers/TestDatabase.cs @@ -14,9 +14,9 @@ namespace Timeline.Tests.Helpers // currently password service is thread safe, so we share a static one. private static PasswordService PasswordService { get; } = new PasswordService(); - private static User CreateEntityFromMock(MockUser user) + private static UserEntity CreateEntityFromMock(MockUser user) { - return new User + return new UserEntity { Name = user.Username, EncryptedPassword = PasswordService.HashPassword(user.Password), @@ -25,7 +25,7 @@ namespace Timeline.Tests.Helpers }; } - private static IEnumerable<User> CreateDefaultMockEntities() + private static IEnumerable<UserEntity> CreateDefaultMockEntities() { // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. yield return CreateEntityFromMock(MockUser.User); diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index fbef6da3..ea9f1177 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -24,7 +24,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdmin();
var res = await client.GetAsync("users");
res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<UserInfo[]>()
+ .And.HaveJsonBody<User[]>()
.Which.Should().BeEquivalentTo(MockUser.UserInfoList);
}
@@ -34,7 +34,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsAdmin();
var res = await client.GetAsync("users/" + MockUser.User.Username);
res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<UserInfo>()
+ .And.HaveJsonBody<User>()
.Which.Should().BeEquivalentTo(MockUser.User.Info);
}
@@ -77,7 +77,7 @@ namespace Timeline.Tests.IntegratedTests {
var res = await client.GetAsync("users/" + username);
res.Should().HaveStatusCode(200)
- .And.HaveJsonBody<UserInfo>()
+ .And.HaveJsonBody<User>()
.Which.Administrator.Should().Be(administrator);
}
diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index 2729aa6f..d4371c48 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -78,7 +78,7 @@ namespace Timeline.Tests.Services public class UserAvatarServiceTest : IDisposable
{
- private UserAvatar CreateMockAvatarEntity(string key) => new UserAvatar
+ private UserAvatarEntity CreateMockAvatarEntity(string key) => new UserAvatarEntity
{
Type = $"image/test{key}",
Data = Encoding.ASCII.GetBytes($"mock{key}"),
@@ -102,7 +102,7 @@ namespace Timeline.Tests.Services Data = Encoding.ASCII.GetBytes($"mock{key}")
};
- private static Avatar ToAvatar(UserAvatar entity)
+ private static Avatar ToAvatar(UserAvatarEntity entity)
{
return new Avatar
{
@@ -111,7 +111,7 @@ namespace Timeline.Tests.Services };
}
- private static AvatarInfo ToAvatarInfo(UserAvatar entity)
+ private static AvatarInfo ToAvatarInfo(UserAvatarEntity entity)
{
return new AvatarInfo
{
diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index 9a869c89..e6eabadf 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -52,7 +52,7 @@ namespace Timeline.Tests.Services {
var context = _testDatabase.Context;
var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id;
- context.UserDetails.Add(new UserDetail
+ context.UserDetails.Add(new UserDetailEntity
{
Nickname = nickname,
UserId = userId
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 67001e87..851c7606 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -94,16 +94,16 @@ namespace Timeline.Controllers User = result
});
}
- catch (JwtVerifyException e)
+ catch (JwtUserTokenBadFormatException e)
{
- if (e.ErrorCode == JwtVerifyException.ErrorCodes.Expired)
+ if (e.ErrorCode == JwtUserTokenBadFormatException.ErrorCodes.Expired)
{
var innerException = e.InnerException as SecurityTokenExpiredException;
LogFailure(LogVerifyExpire, e, ("Expires", innerException?.Expires),
("Current Time", _clock.GetCurrentTime()));
return BadRequest(ErrorResponse.TokenController.Verify_TimeExpired());
}
- else if (e.ErrorCode == JwtVerifyException.ErrorCodes.OldVersion)
+ else if (e.ErrorCode == JwtUserTokenBadFormatException.ErrorCodes.OldVersion)
{
var innerException = e.InnerException as JwtBadVersionException;
LogFailure(LogVerifyOldVersion, e,
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 956865dc..65ee3a0f 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -27,15 +27,15 @@ namespace Timeline.Controllers }
[HttpGet("users"), AdminAuthorize]
- public async Task<ActionResult<UserInfo[]>> List()
+ public async Task<ActionResult<User[]>> List()
{
return Ok(await _userService.ListUsers());
}
[HttpGet("users/{username}"), AdminAuthorize]
- public async Task<ActionResult<UserInfo>> Get([FromRoute][Username] string username)
+ public async Task<ActionResult<User>> Get([FromRoute][Username] string username)
{
- var user = await _userService.GetUser(username);
+ var user = await _userService.GetUserByUsername(username);
if (user == null)
{
_logger.LogInformation(Log.Format(LogGetUserNotExist, ("Username", username)));
diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index ffb6158a..738440b2 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -13,13 +13,13 @@ namespace Timeline.Entities [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")]
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
- modelBuilder.Entity<User>().Property(e => e.Version).HasDefaultValue(0);
- modelBuilder.Entity<User>().HasIndex(e => e.Name).IsUnique();
+ modelBuilder.Entity<UserEntity>().Property(e => e.Version).HasDefaultValue(0);
+ modelBuilder.Entity<UserEntity>().HasIndex(e => e.Name).IsUnique();
}
- public DbSet<User> Users { get; set; } = default!;
- public DbSet<UserAvatar> UserAvatars { get; set; } = default!;
- public DbSet<UserDetail> UserDetails { get; set; } = default!;
+ public DbSet<UserEntity> Users { get; set; } = default!;
+ public DbSet<UserAvatarEntity> UserAvatars { get; set; } = default!;
+ public DbSet<UserDetailEntity> UserDetails { get; set; } = default!;
public DbSet<TimelineEntity> Timelines { get; set; } = default!;
public DbSet<TimelinePostEntity> TimelinePosts { get; set; } = default!;
public DbSet<TimelineMemberEntity> TimelineMembers { get; set; } = default!;
diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 9cacfcae..2bfd6107 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -26,7 +26,7 @@ namespace Timeline.Entities public long OwnerId { get; set; }
[ForeignKey(nameof(OwnerId))]
- public User Owner { get; set; } = default!;
+ public UserEntity Owner { get; set; } = default!;
[Column("visibility")]
public TimelineVisibility Visibility { get; set; }
diff --git a/Timeline/Entities/TimelineMemberEntity.cs b/Timeline/Entities/TimelineMemberEntity.cs index dbe861bd..e76f2099 100644 --- a/Timeline/Entities/TimelineMemberEntity.cs +++ b/Timeline/Entities/TimelineMemberEntity.cs @@ -13,7 +13,7 @@ namespace Timeline.Entities public long UserId { get; set; }
[ForeignKey(nameof(UserId))]
- public User User { get; set; } = default!;
+ public UserEntity User { get; set; } = default!;
[Column("timeline")]
public long TimelineId { get; set; }
diff --git a/Timeline/Entities/TimelinePostEntity.cs b/Timeline/Entities/TimelinePostEntity.cs index efef3ab5..a615c61f 100644 --- a/Timeline/Entities/TimelinePostEntity.cs +++ b/Timeline/Entities/TimelinePostEntity.cs @@ -20,7 +20,7 @@ namespace Timeline.Entities public long AuthorId { get; set; }
[ForeignKey(nameof(AuthorId))]
- public User Author { get; set; } = default!;
+ public UserEntity Author { get; set; } = default!;
[Column("content")]
public string? Content { get; set; }
diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatarEntity.cs index 114246f3..eed819bc 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatarEntity.cs @@ -6,7 +6,7 @@ namespace Timeline.Entities {
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")]
[Table("user_avatars")]
- public class UserAvatar
+ public class UserAvatarEntity
{
[Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetailEntity.cs index 45f87e2b..7a525294 100644 --- a/Timeline/Entities/UserDetail.cs +++ b/Timeline/Entities/UserDetailEntity.cs @@ -1,14 +1,10 @@ -using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-using System.Linq;
-using System.Threading.Tasks;
namespace Timeline.Entities
{
[Table("user_details")]
- public class UserDetail
+ public class UserDetailEntity
{
[Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/UserEntity.cs index e725a69a..83ef5621 100644 --- a/Timeline/Entities/User.cs +++ b/Timeline/Entities/UserEntity.cs @@ -12,7 +12,7 @@ namespace Timeline.Entities [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "This is an entity class.")]
[Table("users")]
- public class User
+ public class UserEntity
{
[Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
@@ -29,9 +29,9 @@ namespace Timeline.Entities [Column("version"), Required]
public long Version { get; set; }
- public UserAvatar? Avatar { get; set; }
+ public UserAvatarEntity? Avatar { get; set; }
- public UserDetail? Detail { get; set; }
+ public UserDetailEntity? Detail { get; set; }
public List<TimelineEntity> Timelines { get; set; } = default!;
diff --git a/Timeline/Models/Http/Token.cs b/Timeline/Models/Http/Token.cs index ea8b59ed..0649f1d1 100644 --- a/Timeline/Models/Http/Token.cs +++ b/Timeline/Models/Http/Token.cs @@ -16,7 +16,7 @@ namespace Timeline.Models.Http public class CreateTokenResponse
{
public string Token { get; set; } = default!;
- public UserInfo User { get; set; } = default!;
+ public User User { get; set; } = default!;
}
public class VerifyTokenRequest
@@ -27,6 +27,6 @@ namespace Timeline.Models.Http public class VerifyTokenResponse
{
- public UserInfo User { get; set; } = default!;
+ public User User { get; set; } = default!;
}
}
diff --git a/Timeline/Models/Http/User.cs b/Timeline/Models/Http/User.cs index 516c1329..69bfacf2 100644 --- a/Timeline/Models/Http/User.cs +++ b/Timeline/Models/Http/User.cs @@ -3,6 +3,12 @@ using Timeline.Models.Validation; namespace Timeline.Models.Http
{
+ public class User
+ {
+ public string Username { get; set; } = default!;
+ public bool Administrator { get; set; }
+ }
+
public class UserPutRequest
{
[Required]
diff --git a/Timeline/Models/UserInfo.cs b/Timeline/Models/UserInfo.cs index b60bdfa2..eff47329 100644 --- a/Timeline/Models/UserInfo.cs +++ b/Timeline/Models/UserInfo.cs @@ -1,23 +1,10 @@ -namespace Timeline.Models
+namespace Timeline.Models
{
- public sealed class UserInfo
+ public class UserInfo
{
- public UserInfo()
- {
- }
-
- public UserInfo(string username, bool administrator)
- {
- Username = username;
- Administrator = administrator;
- }
-
+ public long Id { get; set; }
+ public long Version { get; set; }
public string Username { get; set; } = default!;
- public bool Administrator { get; set; } = default!;
-
- public override string ToString()
- {
- return $"Username: {Username} ; Administrator: {Administrator}";
- }
+ public bool Administrator { get; set; }
}
}
diff --git a/Timeline/Models/UserConvert.cs b/Timeline/Models/UserRoleConvert.cs index 5b132421..ade9a799 100644 --- a/Timeline/Models/UserConvert.cs +++ b/Timeline/Models/UserRoleConvert.cs @@ -2,32 +2,9 @@ using System; using System.Collections.Generic;
using System.Linq;
using Timeline.Entities;
-using Timeline.Services;
namespace Timeline.Models
{
- public static class UserConvert
- {
- public static UserInfo CreateUserInfo(User user)
- {
- if (user == null)
- throw new ArgumentNullException(nameof(user));
- return new UserInfo(user.Name, UserRoleConvert.ToBool(user.RoleString));
- }
-
- internal static UserCache CreateUserCache(User user)
- {
- if (user == null)
- throw new ArgumentNullException(nameof(user));
- return new UserCache
- {
- Username = user.Name,
- Administrator = UserRoleConvert.ToBool(user.RoleString),
- Version = user.Version
- };
- }
- }
-
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "No need.")]
public static class UserRoleConvert
{
diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 1b46f9e9..0a3325d4 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -178,92 +178,65 @@ namespace Timeline.Resources.Services { }
/// <summary>
- /// Looks up a localized string similar to The version of the jwt token is old..
+ /// Looks up a localized string similar to The token didn't pass verification because {0}..
/// </summary>
- internal static string JwtBadVersionException {
+ internal static string JwtUserTokenBadFormatException {
get {
- return ResourceManager.GetString("JwtBadVersionException", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatException", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to The token didn't pass verification because {0}, see inner exception for information..
+ /// Looks up a localized string similar to id claim is not a number.
/// </summary>
- internal static string JwtVerifyException {
+ internal static string JwtUserTokenBadFormatExceptionIdBadFormat {
get {
- return ResourceManager.GetString("JwtVerifyException", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdBadFormat", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to token is expired..
+ /// Looks up a localized string similar to id claim does not exist.
/// </summary>
- internal static string JwtVerifyExceptionExpired {
+ internal static string JwtUserTokenBadFormatExceptionIdMissing {
get {
- return ResourceManager.GetString("JwtVerifyExceptionExpired", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionIdMissing", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to id claim is not a number..
+ /// Looks up a localized string similar to other error, see inner exception for information.
/// </summary>
- internal static string JwtVerifyExceptionIdClaimBadFormat {
+ internal static string JwtUserTokenBadFormatExceptionOthers {
get {
- return ResourceManager.GetString("JwtVerifyExceptionIdClaimBadFormat", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionOthers", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to id claim does not exist..
- /// </summary>
- internal static string JwtVerifyExceptionNoIdClaim {
- get {
- return ResourceManager.GetString("JwtVerifyExceptionNoIdClaim", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to version claim does not exist..
- /// </summary>
- internal static string JwtVerifyExceptionNoVersionClaim {
- get {
- return ResourceManager.GetString("JwtVerifyExceptionNoVersionClaim", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to version of token is old..
- /// </summary>
- internal static string JwtVerifyExceptionOldVersion {
- get {
- return ResourceManager.GetString("JwtVerifyExceptionOldVersion", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to uncommon error..
+ /// Looks up a localized string similar to unknown error.
/// </summary>
- internal static string JwtVerifyExceptionOthers {
+ internal static string JwtUserTokenBadFormatExceptionUnknown {
get {
- return ResourceManager.GetString("JwtVerifyExceptionOthers", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionUnknown", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to unknown error code..
+ /// Looks up a localized string similar to version claim is not a number..
/// </summary>
- internal static string JwtVerifyExceptionUnknown {
+ internal static string JwtUserTokenBadFormatExceptionVersionBadFormat {
get {
- return ResourceManager.GetString("JwtVerifyExceptionUnknown", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionBadFormat", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to version claim is not a number..
+ /// Looks up a localized string similar to version claim does not exist..
/// </summary>
- internal static string JwtVerifyExceptionVersionClaimBadFormat {
+ internal static string JwtUserTokenBadFormatExceptionVersionMissing {
get {
- return ResourceManager.GetString("JwtVerifyExceptionVersionClaimBadFormat", resourceCulture);
+ return ResourceManager.GetString("JwtUserTokenBadFormatExceptionVersionMissing", resourceCulture);
}
}
@@ -356,5 +329,32 @@ namespace Timeline.Resources.Services { return ResourceManager.GetString("UserNotExistException", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad format, which means it may not be created by the server..
+ /// </summary>
+ internal static string UserTokenBadFormatException {
+ get {
+ return ResourceManager.GetString("UserTokenBadFormatException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is of bad version..
+ /// </summary>
+ internal static string UserTokenBadVersionException {
+ get {
+ return ResourceManager.GetString("UserTokenBadVersionException", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The token is expired because its expiration time has passed..
+ /// </summary>
+ internal static string UserTokenTimeExpireException {
+ get {
+ return ResourceManager.GetString("UserTokenTimeExpireException", resourceCulture);
+ }
+ }
}
}
diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 1d9c0037..bc96248d 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -156,36 +156,27 @@ <data name="HashedPasswordBadFromatExceptionNotUnknownMarker" xml:space="preserve">
<value>Unknown format marker.</value>
</data>
- <data name="JwtBadVersionException" xml:space="preserve">
- <value>The version of the jwt token is old.</value>
+ <data name="JwtUserTokenBadFormatException" xml:space="preserve">
+ <value>The token didn't pass verification because {0}.</value>
</data>
- <data name="JwtVerifyException" xml:space="preserve">
- <value>The token didn't pass verification because {0}, see inner exception for information.</value>
+ <data name="JwtUserTokenBadFormatExceptionIdBadFormat" xml:space="preserve">
+ <value>id claim is not a number</value>
</data>
- <data name="JwtVerifyExceptionExpired" xml:space="preserve">
- <value>token is expired.</value>
+ <data name="JwtUserTokenBadFormatExceptionIdMissing" xml:space="preserve">
+ <value>id claim does not exist</value>
</data>
- <data name="JwtVerifyExceptionIdClaimBadFormat" xml:space="preserve">
- <value>id claim is not a number.</value>
+ <data name="JwtUserTokenBadFormatExceptionOthers" xml:space="preserve">
+ <value>other error, see inner exception for information</value>
</data>
- <data name="JwtVerifyExceptionNoIdClaim" xml:space="preserve">
- <value>id claim does not exist.</value>
- </data>
- <data name="JwtVerifyExceptionNoVersionClaim" xml:space="preserve">
- <value>version claim does not exist.</value>
- </data>
- <data name="JwtVerifyExceptionOldVersion" xml:space="preserve">
- <value>version of token is old.</value>
- </data>
- <data name="JwtVerifyExceptionOthers" xml:space="preserve">
- <value>uncommon error.</value>
- </data>
- <data name="JwtVerifyExceptionUnknown" xml:space="preserve">
- <value>unknown error code.</value>
+ <data name="JwtUserTokenBadFormatExceptionUnknown" xml:space="preserve">
+ <value>unknown error</value>
</data>
- <data name="JwtVerifyExceptionVersionClaimBadFormat" xml:space="preserve">
+ <data name="JwtUserTokenBadFormatExceptionVersionBadFormat" xml:space="preserve">
<value>version claim is not a number.</value>
</data>
+ <data name="JwtUserTokenBadFormatExceptionVersionMissing" xml:space="preserve">
+ <value>version claim does not exist.</value>
+ </data>
<data name="TimelineAlreadyExistException" xml:space="preserve">
<value>The timeline with that name already exists.</value>
</data>
@@ -216,4 +207,13 @@ <data name="UserNotExistException" xml:space="preserve">
<value>The user does not exist.</value>
</data>
+ <data name="UserTokenBadFormatException" xml:space="preserve">
+ <value>The token is of bad format, which means it may not be created by the server.</value>
+ </data>
+ <data name="UserTokenBadVersionException" xml:space="preserve">
+ <value>The token is of bad version.</value>
+ </data>
+ <data name="UserTokenTimeExpireException" xml:space="preserve">
+ <value>The token is expired because its expiration time has passed.</value>
+ </data>
</root>
\ No newline at end of file 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; } = "";
}
}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 5b6499a4..c1e73182 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -84,7 +84,7 @@ namespace Timeline });
services.AddScoped<IUserService, UserService>();
- services.AddScoped<IJwtService, JwtService>();
+ services.AddScoped<IJwtService, UserTokenService>();
services.AddTransient<IPasswordService, PasswordService>();
services.AddTransient<IClock, Clock>();
services.AddUserAvatarService();
|