From da9139b7bab95f6e5ba5f4bb2d99011c2d6db03a Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 23 Mar 2022 21:30:14 +0800 Subject: … MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BackEnd/Timeline.Tests/Helpers/TestDatabase.cs | 7 +- .../Timeline.Tests/IntegratedTests/TokenTest.cs | 42 +- BackEnd/Timeline.Tests/Services/ServiceTestBase.cs | 10 +- BackEnd/Timeline/Auth/MyAuthenticationHandler.cs | 29 +- BackEnd/Timeline/Auth/Resource.Designer.cs | 294 +++++----- BackEnd/Timeline/Auth/Resource.resx | 17 +- BackEnd/Timeline/Configs/JwtOptions.cs | 8 - BackEnd/Timeline/Configs/TokenOptions.cs | 9 +- .../Controllers/BookmarkTimelineController.cs | 8 +- .../Controllers/HighlightTimelineController.cs | 4 +- BackEnd/Timeline/Controllers/MyControllerBase.cs | 33 +- BackEnd/Timeline/Controllers/Resource.Designer.cs | 393 ++++++------- BackEnd/Timeline/Controllers/Resource.resx | 14 +- BackEnd/Timeline/Controllers/TimelineController.cs | 10 +- .../Timeline/Controllers/TimelinePostController.cs | 12 +- BackEnd/Timeline/Controllers/TokenController.cs | 37 +- .../Timeline/Controllers/UserAvatarController.cs | 4 +- BackEnd/Timeline/Controllers/UserController.cs | 7 +- BackEnd/Timeline/Entities/UserTokenEntity.cs | 3 + BackEnd/Timeline/ErrorCodes.cs | 11 +- .../20220323073853_AddDeletedToToken.Designer.cs | 628 +++++++++++++++++++++ .../Migrations/20220323073853_AddDeletedToToken.cs | 26 + .../Migrations/DatabaseContextModelSnapshot.cs | 4 + .../Services/Token/DatabaseUserTokenHandler.cs | 0 .../Timeline/Services/Token/IUserTokenHandler.cs | 29 - .../Timeline/Services/Token/IUserTokenManager.cs | 35 -- .../Timeline/Services/Token/IUserTokenService.cs | 45 ++ .../Token/JwtUserTokenBadFormatException.cs | 47 -- .../Timeline/Services/Token/Resource.Designer.cs | 276 +++------ BackEnd/Timeline/Services/Token/Resource.resx | 165 +++--- .../Services/Token/SecureRandomUserTokenService.cs | 125 ++++ .../TokenServicesServiceColletionExtensions.cs | 4 +- .../Services/Token/UserTokenBadFormatException.cs | 17 - .../Services/Token/UserTokenExpiredException.cs | 21 + .../Timeline/Services/Token/UserTokenHandler.cs | 117 ---- BackEnd/Timeline/Services/Token/UserTokenInfo.cs | 6 +- .../Timeline/Services/Token/UserTokenManager.cs | 102 ---- .../Token/UserTokenTimeExpiredException.cs | 21 - .../Token/UserTokenUserNotExistException.cs | 16 - .../Token/UserTokenVersionExpiredException.cs | 21 - .../Timeline/Services/User/CreateTokenResult.cs | 12 + BackEnd/Timeline/Services/User/UserService.cs | 15 +- BackEnd/Timeline/Timeline.csproj | 3 + BackEnd/Timeline/appsettings.json | 4 - 44 files changed, 1448 insertions(+), 1243 deletions(-) delete mode 100644 BackEnd/Timeline/Configs/JwtOptions.cs create mode 100644 BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.cs delete mode 100644 BackEnd/Timeline/Services/Token/DatabaseUserTokenHandler.cs delete mode 100644 BackEnd/Timeline/Services/Token/IUserTokenHandler.cs delete mode 100644 BackEnd/Timeline/Services/Token/IUserTokenManager.cs create mode 100644 BackEnd/Timeline/Services/Token/IUserTokenService.cs delete mode 100644 BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs create mode 100644 BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs delete mode 100644 BackEnd/Timeline/Services/Token/UserTokenBadFormatException.cs create mode 100644 BackEnd/Timeline/Services/Token/UserTokenExpiredException.cs delete mode 100644 BackEnd/Timeline/Services/Token/UserTokenHandler.cs delete mode 100644 BackEnd/Timeline/Services/Token/UserTokenManager.cs delete mode 100644 BackEnd/Timeline/Services/Token/UserTokenTimeExpiredException.cs delete mode 100644 BackEnd/Timeline/Services/Token/UserTokenUserNotExistException.cs delete mode 100644 BackEnd/Timeline/Services/Token/UserTokenVersionExpiredException.cs create mode 100644 BackEnd/Timeline/Services/User/CreateTokenResult.cs (limited to 'BackEnd') diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs index 9bd690a2..5752b8cd 100644 --- a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -1,9 +1,11 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services; +using Timeline.Services.Token; using Timeline.Services.User; using Xunit; using Xunit.Abstractions; @@ -24,7 +26,10 @@ namespace Timeline.Tests.Helpers using var context = CreateContext(); await context.Database.MigrateAsync(); - var userService = new UserService(NullLogger.Instance, context, new PasswordService(), new Clock()); + var mockUserTokenManager = new Mock(); + mockUserTokenManager.SetReturnsDefault(Task.CompletedTask); + + var userService = new UserService(NullLogger.Instance, context, new PasswordService(), mockUserTokenManager.Object, new Clock()); await userService.ModifyUserAsync( await userService.GetUserIdByUsernameAsync("administrator"), diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs index 68681b61..555ab4da 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -1,10 +1,8 @@ using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Timeline.Models.Http; -using Timeline.Services.User; using Xunit; using Xunit.Abstractions; @@ -80,48 +78,12 @@ namespace Timeline.Tests.IntegratedTests } [Fact] - public async Task VerifyToken_BadFormat() + public async Task VerifyToken_Invalid() { using var client = await CreateDefaultClient(); await client.TestPostAssertErrorAsync(VerifyTokenUrl, new HttpVerifyTokenRequest { Token = "bad token hahaha" }, - errorCode: ErrorCodes.TokenController.VerifyBadFormat); - } - - [Fact] - public async Task VerifyToken_OldVersion() - { - using var client = await CreateDefaultClient(); - var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; - - using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped. - { - // create a user for test - var userService = scope.ServiceProvider.GetRequiredService(); - var id = await userService.GetUserIdByUsernameAsync("user1"); - await userService.ModifyUserAsync(id, new ModifyUserParams { Password = "user1pw" }); - } - - await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new HttpVerifyTokenRequest { Token = token }, - errorCode: ErrorCodes.TokenController.VerifyOldVersion); - } - - [Fact] - public async Task VerifyToken_UserNotExist() - { - using var client = await CreateDefaultClient(); - var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; - - using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped. - { - var userService = scope.ServiceProvider.GetRequiredService(); - await userService.DeleteUserAsync("user1"); - } - - await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new HttpVerifyTokenRequest { Token = token }, - errorCode: ErrorCodes.TokenController.VerifyUserNotExist); + errorCode: ErrorCodes.TokenController.VerifyInvalid); } //[Fact] diff --git a/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs b/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs index 7153e99b..fea31d0a 100644 --- a/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs +++ b/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Logging.Abstractions; +using Moq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services.Timeline; +using Timeline.Services.Token; using Timeline.Services.User; using Timeline.Tests.Helpers; using Xunit; @@ -19,6 +21,8 @@ namespace Timeline.Tests.Services protected TestClock Clock { get; } = new TestClock(); protected UserService UserService { get; private set; } = default!; protected TimelineService TimelineService { get; private set; } = default!; + protected Mock UserTokenServiceMock { get; private set; } = default!; + protected IUserTokenService UserTokenService { get; private set; } = default!; protected long UserId { get; private set; } protected long AdminId { get; private set; } @@ -34,7 +38,11 @@ namespace Timeline.Tests.Services await TestDatabase.InitializeAsync(); Database = TestDatabase.CreateContext(_testOutputHelper); - UserService = new UserService(NullLogger.Instance, Database, new PasswordService(), Clock); + UserTokenServiceMock = new(); + UserTokenServiceMock.SetReturnsDefault(Task.CompletedTask); + UserTokenService = UserTokenServiceMock.Object; + + UserService = new UserService(NullLogger.Instance, Database, new PasswordService(), UserTokenService, Clock); TimelineService = new TimelineService(NullLoggerFactory.Instance, Database, UserService, Clock); UserId = await UserService.GetUserIdByUsernameAsync("user"); diff --git a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs index 016cc938..740f5aee 100644 --- a/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs +++ b/BackEnd/Timeline/Auth/MyAuthenticationHandler.cs @@ -41,11 +41,8 @@ namespace Timeline.Auth { return e switch { - UserTokenTimeExpiredException => ErrorCodes.Common.Token.TimeExpired, - UserTokenVersionExpiredException => ErrorCodes.Common.Token.VersionExpired, - UserTokenBadFormatException => ErrorCodes.Common.Token.BadFormat, - UserTokenUserNotExistException => ErrorCodes.Common.Token.UserNotExist, - _ => ErrorCodes.Common.Token.Unknown + UserTokenExpiredException => ErrorCodes.Common.Token.TimeExpired, + _ => ErrorCodes.Common.Token.Invalid }; } @@ -53,25 +50,22 @@ namespace Timeline.Auth { return errorCode switch { - ErrorCodes.Common.Token.TimeExpired => Resource.MessageTokenTimeExpired, - ErrorCodes.Common.Token.VersionExpired => Resource.MessageTokenVersionExpired, - ErrorCodes.Common.Token.BadFormat => Resource.MessageTokenBadFormat, - ErrorCodes.Common.Token.UserNotExist => Resource.MessageTokenUserNotExist, - _ => Resource.MessageTokenUnknownError + ErrorCodes.Common.Token.TimeExpired => Resource.MessageTokenExpired, + _ => Resource.MessageTokenInvalid }; } private readonly ILogger _logger; - private readonly IUserTokenManager _userTokenManager; + private readonly IUserTokenService _userTokenService; private readonly IUserPermissionService _userPermissionService; private readonly IOptionsMonitor _jsonOptions; - public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenManager userTokenManager, IUserPermissionService userPermissionService, IOptionsMonitor jsonOptions) + public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserTokenService userTokenService, IUserPermissionService userPermissionService, IOptionsMonitor jsonOptions) : base(options, logger, encoder, clock) { _logger = logger.CreateLogger(); - _userTokenManager = userTokenManager; + _userTokenService = userTokenService; _userPermissionService = userPermissionService; _jsonOptions = jsonOptions; } @@ -126,13 +120,12 @@ namespace Timeline.Auth try { - var user = await _userTokenManager.VerifyTokenAsync(token); + var userTokenInfo = await _userTokenService.ValidateTokenAsync(token); var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); - identity.AddClaim(new Claim(identity.NameClaimType, user.Username, ClaimValueTypes.String)); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userTokenInfo.UserId.ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64)); - var permissions = await _userPermissionService.GetPermissionsOfUserAsync(user.Id); + var permissions = await _userPermissionService.GetPermissionsOfUserAsync(userTokenInfo.UserId); identity.AddClaims(permissions.Select(permission => new Claim(AuthenticationConstants.PermissionClaimName, permission.ToString(), ClaimValueTypes.String))); var principal = new ClaimsPrincipal(); @@ -161,7 +154,7 @@ namespace Timeline.Auth if (properties.Items.TryGetValue(TokenErrorCodeKey, out var tokenErrorCode)) { if (!int.TryParse(tokenErrorCode, out var errorCode)) - errorCode = ErrorCodes.Common.Token.Unknown; + throw new Exception("A logic error: failed to parse token error code."); body = new CommonResponse(errorCode, GetTokenErrorMessageFromErrorCode(errorCode)); } else diff --git a/BackEnd/Timeline/Auth/Resource.Designer.cs b/BackEnd/Timeline/Auth/Resource.Designer.cs index 05394551..6175b266 100644 --- a/BackEnd/Timeline/Auth/Resource.Designer.cs +++ b/BackEnd/Timeline/Auth/Resource.Designer.cs @@ -1,162 +1,132 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Auth { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Auth.Resource", typeof(Resource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to User identitifier claim is of bad format.. - /// - internal static string ExceptionUserIdentifierClaimBadFormat { - get { - return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token is found in authorization header. Token is {0} .. - /// - internal static string LogTokenFoundInHeader { - get { - return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} .. - /// - internal static string LogTokenFoundInQuery { - get { - return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No jwt token is found.. - /// - internal static string LogTokenNotFound { - get { - return ResourceManager.GetString("LogTokenNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A jwt token validation failed. Error reason: {0}. - /// - internal static string LogTokenValidationFail { - get { - return ResourceManager.GetString("LogTokenValidationFail", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You must use a token to authenticate to access this resource.. - /// - internal static string MessageNoToken { - get { - return ResourceManager.GetString("MessageNoToken", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format. It might not be created by this server.. - /// - internal static string MessageTokenBadFormat { - get { - return ResourceManager.GetString("MessageTokenBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is out of date and expired. Please create a new one.. - /// - internal static string MessageTokenTimeExpired { - get { - return ResourceManager.GetString("MessageTokenTimeExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A unknown error occured when verify token.. - /// - internal static string MessageTokenUnknownError { - get { - return ResourceManager.GetString("MessageTokenUnknownError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The owner of the token does not exist. It might have been deleted.. - /// - internal static string MessageTokenUserNotExist { - get { - return ResourceManager.GetString("MessageTokenUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of old version and expired. Please create a new one.. - /// - internal static string MessageTokenVersionExpired { - get { - return ResourceManager.GetString("MessageTokenVersionExpired", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Auth { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// This class was generated by MSBuild using the GenerateResource task. + /// To add or remove a member, edit your .resx file then rerun MSBuild. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Auth.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to User identitifier claim is of bad format.. + /// + internal static string ExceptionUserIdentifierClaimBadFormat { + get { + return ResourceManager.GetString("ExceptionUserIdentifierClaimBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token is found in authorization header. Token is {0} .. + /// + internal static string LogTokenFoundInHeader { + get { + return ResourceManager.GetString("LogTokenFoundInHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Token is found in query param with key "{0}". Token is {1} .. + /// + internal static string LogTokenFoundInQuery { + get { + return ResourceManager.GetString("LogTokenFoundInQuery", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No jwt token is found.. + /// + internal static string LogTokenNotFound { + get { + return ResourceManager.GetString("LogTokenNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A jwt token validation failed. Error reason: {0}. + /// + internal static string LogTokenValidationFail { + get { + return ResourceManager.GetString("LogTokenValidationFail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must use a token to authenticate to access this resource.. + /// + internal static string MessageNoToken { + get { + return ResourceManager.GetString("MessageNoToken", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired. Please create a new one.. + /// + internal static string MessageTokenExpired { + get { + return ResourceManager.GetString("MessageTokenExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is invalid.. + /// + internal static string MessageTokenInvalid { + get { + return ResourceManager.GetString("MessageTokenInvalid", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Auth/Resource.resx b/BackEnd/Timeline/Auth/Resource.resx index 88cdbd6b..a2183290 100644 --- a/BackEnd/Timeline/Auth/Resource.resx +++ b/BackEnd/Timeline/Auth/Resource.resx @@ -135,19 +135,10 @@ You must use a token to authenticate to access this resource. - - The token is of bad format. It might not be created by this server. + + The token is invalid. - - The token is out of date and expired. Please create a new one. - - - A unknown error occured when verify token. - - - The owner of the token does not exist. It might have been deleted. - - - The token is of old version and expired. Please create a new one. + + The token is expired. Please create a new one. \ No newline at end of file diff --git a/BackEnd/Timeline/Configs/JwtOptions.cs b/BackEnd/Timeline/Configs/JwtOptions.cs deleted file mode 100644 index c400b8a6..00000000 --- a/BackEnd/Timeline/Configs/JwtOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 index e7d4d9e7..d8e968c7 100644 --- a/BackEnd/Timeline/Configs/TokenOptions.cs +++ b/BackEnd/Timeline/Configs/TokenOptions.cs @@ -2,10 +2,11 @@ { public class TokenOptions { - /// - /// Set the default value of expire offset of jwt token. - /// Unit is second. Default is 3600 * 24 seconds, aka 1 day. + /// + /// The length of the generated secure random token counted in byte. + /// Note the byte will be converted to hex form when used. + /// Default is 32 byte long. /// - public long DefaultExpireSeconds { get; set; } = 3600 * 24; + public long? TokenLength { get; set; } } } diff --git a/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs b/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs index 551a41e2..a1fa511c 100644 --- a/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs +++ b/BackEnd/Timeline/Controllers/BookmarkTimelineController.cs @@ -44,7 +44,7 @@ namespace Timeline.Controllers [ProducesResponseType(401)] public async Task>> List() { - var ids = await _service.GetBookmarksAsync(GetUserId()); + var ids = await _service.GetBookmarksAsync(GetAuthUserId()); var timelines = await _timelineService.GetTimelineList(ids); return await Map(timelines); } @@ -61,7 +61,7 @@ namespace Timeline.Controllers public async Task> Put([GeneralTimelineName] string timeline) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - var create = await _service.AddBookmarkAsync(GetUserId(), timelineId); + var create = await _service.AddBookmarkAsync(GetAuthUserId(), timelineId); return CommonPutResponse.Create(create); } @@ -77,7 +77,7 @@ namespace Timeline.Controllers public async Task> Delete([GeneralTimelineName] string timeline) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - var delete = await _service.RemoveBookmarkAsync(GetUserId(), timelineId); + var delete = await _service.RemoveBookmarkAsync(GetAuthUserId(), timelineId); return CommonDeleteResponse.Create(delete); } @@ -93,7 +93,7 @@ namespace Timeline.Controllers public async Task Move([FromBody] HttpBookmarkTimelineMoveRequest request) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(request.Timeline); - await _service.MoveBookmarkAsync(GetUserId(), timelineId, request.NewPosition!.Value); + await _service.MoveBookmarkAsync(GetAuthUserId(), timelineId, request.NewPosition!.Value); return OkWithCommonResponse(); } } diff --git a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs index 127392db..e30cf720 100644 --- a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs +++ b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs @@ -61,7 +61,7 @@ namespace Timeline.Controllers public async Task> Put([GeneralTimelineName] string timeline) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - var create = await _service.AddHighlightTimelineAsync(timelineId, GetUserId()); + var create = await _service.AddHighlightTimelineAsync(timelineId, GetAuthUserId()); return CommonPutResponse.Create(create); } @@ -78,7 +78,7 @@ namespace Timeline.Controllers public async Task> Delete([GeneralTimelineName] string timeline) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - var delete = await _service.RemoveHighlightTimelineAsync(timelineId, GetUserId()); + var delete = await _service.RemoveHighlightTimelineAsync(timelineId, GetAuthUserId()); return CommonDeleteResponse.Create(delete); } diff --git a/BackEnd/Timeline/Controllers/MyControllerBase.cs b/BackEnd/Timeline/Controllers/MyControllerBase.cs index d4ee9d3e..b74193f4 100644 --- a/BackEnd/Timeline/Controllers/MyControllerBase.cs +++ b/BackEnd/Timeline/Controllers/MyControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using System; +using System.Threading.Tasks; using Timeline.Auth; using Timeline.Models.Http; +using Timeline.Services; using Timeline.Services.User; namespace Timeline.Controllers @@ -15,24 +18,30 @@ namespace Timeline.Controllers return User.HasPermission(permission); } - protected string? GetOptionalUsername() + protected long? GetOptionalAuthUserId() { - return User.GetOptionalName(); - } - - protected string GetUsername() - { - return GetOptionalUsername() ?? throw new InvalidOperationException(Resource.ExceptionNoUsername); + return User.GetOptionalUserId(); } - protected long? GetOptionalUserId() + protected long GetAuthUserId() { - return User.GetOptionalUserId(); + return GetOptionalAuthUserId() ?? throw new InvalidOperationException(Resource.ExceptionNoUserId); } - protected long GetUserId() - { - return GetOptionalUserId() ?? throw new InvalidOperationException(Resource.ExceptionNoUserId); + protected async Task CheckIsSelf(string username) + { + var authUserId = GetOptionalAuthUserId(); + if (!authUserId.HasValue) return false; + try + { + var userService = HttpContext.RequestServices.GetRequiredService(); + var id = await userService.GetUserIdByUsernameAsync(username); + return authUserId == id; + } + catch (EntityNotExistException) + { + return false; + } } #endregion auth diff --git a/BackEnd/Timeline/Controllers/Resource.Designer.cs b/BackEnd/Timeline/Controllers/Resource.Designer.cs index a647558a..eeb2f0fa 100644 --- a/BackEnd/Timeline/Controllers/Resource.Designer.cs +++ b/BackEnd/Timeline/Controllers/Resource.Designer.cs @@ -1,207 +1,186 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Controllers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Controllers.Resource", typeof(Resource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Can't get user id.. - /// - internal static string ExceptionNoUserId { - get { - return ResourceManager.GetString("ExceptionNoUserId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Can't get username.. - /// - internal static string ExceptionNoUsername { - get { - return ResourceManager.GetString("ExceptionNoUsername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You have no permission to access this.. - /// - internal static string MessageForbid { - get { - return ResourceManager.GetString("MessageForbid", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't do this unless you are administrator.. - /// - internal static string MessageForbidNotAdministrator { - get { - return ResourceManager.GetString("MessageForbidNotAdministrator", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't do this unless you are administrator or resource owner.. - /// - internal static string MessageForbidNotAdministratorOrOwner { - get { - return ResourceManager.GetString("MessageForbidNotAdministratorOrOwner", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can't do this because it is the root user.. - /// - internal static string MessageInvalidOperationOnRootUser { - get { - return ResourceManager.GetString("MessageInvalidOperationOnRootUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The old password is wrong.. - /// - internal static string MessageOldPasswordWrong { - get { - return ResourceManager.GetString("MessageOldPasswordWrong", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Operation succeeded.. - /// - internal static string MessageOperationSucceeded { - get { - return ResourceManager.GetString("MessageOperationSucceeded", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The user specified by query param "relate" does not exist.. - /// - internal static string MessageTimelineListQueryRelateNotExist { - get { - return ResourceManager.GetString("MessageTimelineListQueryRelateNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to '{0}' is an unkown visibility in the query parameter 'visibility'. . - /// - internal static string MessageTimelineListQueryVisibilityUnknown { - get { - return ResourceManager.GetString("MessageTimelineListQueryVisibilityUnknown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Username or password is invalid.. - /// - internal static string MessageTokenCreateBadCredential { - get { - return ResourceManager.GetString("MessageTokenCreateBadCredential", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format. It might not be created by the server.. - /// - internal static string MessageTokenVerifyBadFormat { - get { - return ResourceManager.GetString("MessageTokenVerifyBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Token has an old version. User might have update some info.. - /// - internal static string MessageTokenVerifyOldVersion { - get { - return ResourceManager.GetString("MessageTokenVerifyOldVersion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired.. - /// - internal static string MessageTokenVerifyTimeExpired { - get { - return ResourceManager.GetString("MessageTokenVerifyTimeExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to User does not exist. Administrator might have deleted this user.. - /// - internal static string MessageTokenVerifyUserNotExist { - get { - return ResourceManager.GetString("MessageTokenVerifyUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A user with given username already exists.. - /// - internal static string MessageUsernameConflict { - get { - return ResourceManager.GetString("MessageUsernameConflict", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// This class was generated by MSBuild using the GenerateResource task. + /// To add or remove a member, edit your .resx file then rerun MSBuild. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Controllers.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Can't get user id.. + /// + internal static string ExceptionNoUserId { + get { + return ResourceManager.GetString("ExceptionNoUserId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't get username.. + /// + internal static string ExceptionNoUsername { + get { + return ResourceManager.GetString("ExceptionNoUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You have no permission to access this.. + /// + internal static string MessageForbid { + get { + return ResourceManager.GetString("MessageForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't do this unless you are administrator.. + /// + internal static string MessageForbidNotAdministrator { + get { + return ResourceManager.GetString("MessageForbidNotAdministrator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't do this unless you are administrator or resource owner.. + /// + internal static string MessageForbidNotAdministratorOrOwner { + get { + return ResourceManager.GetString("MessageForbidNotAdministratorOrOwner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can't do this because it is the root user.. + /// + internal static string MessageInvalidOperationOnRootUser { + get { + return ResourceManager.GetString("MessageInvalidOperationOnRootUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The old password is wrong.. + /// + internal static string MessageOldPasswordWrong { + get { + return ResourceManager.GetString("MessageOldPasswordWrong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Operation succeeded.. + /// + internal static string MessageOperationSucceeded { + get { + return ResourceManager.GetString("MessageOperationSucceeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The user specified by query param "relate" does not exist.. + /// + internal static string MessageTimelineListQueryRelateNotExist { + get { + return ResourceManager.GetString("MessageTimelineListQueryRelateNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is an unkown visibility in the query parameter 'visibility'. . + /// + internal static string MessageTimelineListQueryVisibilityUnknown { + get { + return ResourceManager.GetString("MessageTimelineListQueryVisibilityUnknown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Username or password is invalid.. + /// + internal static string MessageTokenCreateBadCredential { + get { + return ResourceManager.GetString("MessageTokenCreateBadCredential", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is expired. Please create a new one.. + /// + internal static string MessageTokenVerifyExpired { + get { + return ResourceManager.GetString("MessageTokenVerifyExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is invalid.. + /// + internal static string MessageTokenVerifyInvalid { + get { + return ResourceManager.GetString("MessageTokenVerifyInvalid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user with given username already exists.. + /// + internal static string MessageUsernameConflict { + get { + return ResourceManager.GetString("MessageUsernameConflict", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Controllers/Resource.resx b/BackEnd/Timeline/Controllers/Resource.resx index 47b7a329..b70e5230 100644 --- a/BackEnd/Timeline/Controllers/Resource.resx +++ b/BackEnd/Timeline/Controllers/Resource.resx @@ -150,17 +150,11 @@ Username or password is invalid. - - The token is of bad format. It might not be created by the server. + + The token is invalid. - - Token has an old version. User might have update some info. - - - The token is expired. - - - User does not exist. Administrator might have deleted this user. + + The token is expired. Please create a new one. A user with given username already exists. diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index f98ff3e0..42b8f210 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -142,7 +142,7 @@ namespace Timeline.Controllers { var timelineId = await _service.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermissionAsync(timelineId, GetUserId())) + if (!UserHasAllTimelineManagementPermission && !await _service.HasManagePermissionAsync(timelineId, GetAuthUserId())) { return ForbidWithCommonResponse(); } @@ -168,7 +168,7 @@ namespace Timeline.Controllers { var timelineId = await _service.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermissionAsync(timelineId, GetUserId()))) + if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermissionAsync(timelineId, GetAuthUserId()))) { return ForbidWithCommonResponse(); } @@ -194,7 +194,7 @@ namespace Timeline.Controllers { var timelineId = await _service.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermissionAsync(timelineId, GetUserId()))) + if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermissionAsync(timelineId, GetAuthUserId()))) { return ForbidWithCommonResponse(); } @@ -216,7 +216,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> TimelineCreate([FromBody] HttpTimelineCreateRequest body) { - var userId = GetUserId(); + var userId = GetAuthUserId(); var timeline = await _service.CreateTimelineAsync(body.Name, userId); var result = await Map(timeline); @@ -240,7 +240,7 @@ namespace Timeline.Controllers { var timelineId = await _service.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermissionAsync(timelineId, GetUserId()))) + if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermissionAsync(timelineId, GetAuthUserId()))) { return ForbidWithCommonResponse(); } diff --git a/BackEnd/Timeline/Controllers/TimelinePostController.cs b/BackEnd/Timeline/Controllers/TimelinePostController.cs index f00a689c..c49c95fc 100644 --- a/BackEnd/Timeline/Controllers/TimelinePostController.cs +++ b/BackEnd/Timeline/Controllers/TimelinePostController.cs @@ -77,7 +77,7 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalUserId())) + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) { return ForbidWithCommonResponse(); } @@ -102,7 +102,7 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalUserId())) + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) { return ForbidWithCommonResponse(); } @@ -148,7 +148,7 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalUserId())) + if (!UserHasAllTimelineManagementPermission && !await _timelineService.HasReadPermissionAsync(timelineId, GetOptionalAuthUserId())) { return ForbidWithCommonResponse(); } @@ -182,7 +182,7 @@ namespace Timeline.Controllers public async Task> Post([FromRoute][GeneralTimelineName] string timeline, [FromBody] HttpTimelinePostCreateRequest body) { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - var userId = GetUserId(); + var userId = GetAuthUserId(); if (!UserHasAllTimelineManagementPermission && !await _timelineService.IsMemberOfAsync(timelineId, userId)) { @@ -247,7 +247,7 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetUserId(), true)) + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) { return ForbidWithCommonResponse(); } @@ -274,7 +274,7 @@ namespace Timeline.Controllers { var timelineId = await _timelineService.GetTimelineIdByNameAsync(timeline); - if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetUserId(), true)) + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermissionAsync(timelineId, post, GetAuthUserId(), true)) { return ForbidWithCommonResponse(); } diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index ae3e1b94..9ee5a09f 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -19,13 +19,15 @@ namespace Timeline.Controllers [ProducesErrorResponseType(typeof(CommonResponse))] public class TokenController : MyControllerBase { - private readonly IUserTokenManager _userTokenManager; + private readonly IUserService _userService; + private readonly IUserTokenService _userTokenService; private readonly IGenericMapper _mapper; private readonly IClock _clock; - public TokenController(IUserTokenManager userTokenManager, IGenericMapper mapper, IClock clock) + public TokenController(IUserService userService, IUserTokenService userTokenService, IGenericMapper mapper, IClock clock) { - _userTokenManager = userTokenManager; + _userService = userService; + _userTokenService = userTokenService; _mapper = mapper; _clock = clock; } @@ -47,12 +49,14 @@ namespace Timeline.Controllers if (request.Expire is not null) expireTime = _clock.GetCurrentTime().AddDays(request.Expire.Value); - var result = await _userTokenManager.CreateTokenAsync(request.Username, request.Password, expireTime); + var userId = await _userService.VerifyCredential(request.Username, request.Password); + var token = await _userTokenService.CreateTokenAsync(userId, expireTime); + var user = await _userService.GetUserAsync(userId); return new HttpCreateTokenResponse { - Token = result.Token, - User = await _mapper.MapAsync(result.User, Url, User) + Token = token, + User = await _mapper.MapAsync(user, Url, User) }; } catch (EntityNotExistException) @@ -77,27 +81,20 @@ namespace Timeline.Controllers { try { - var result = await _userTokenManager.VerifyTokenAsync(request.Token); + var tokenInfo = await _userTokenService.ValidateTokenAsync(request.Token); + var user = await _userService.GetUserAsync(tokenInfo.UserId); return new HttpVerifyTokenResponse { - User = await _mapper.MapAsync(result, Url, User) + User = await _mapper.MapAsync(user, Url, User) }; } - catch (UserTokenTimeExpiredException) + catch (UserTokenExpiredException) { - return BadRequestWithCommonResponse(ErrorCodes.TokenController.VerifyTimeExpired, Resource.MessageTokenVerifyTimeExpired); + return BadRequestWithCommonResponse(ErrorCodes.TokenController.VerifyExpired, Resource.MessageTokenVerifyExpired); } - catch (UserTokenVersionExpiredException) + catch (UserTokenException) { - return BadRequestWithCommonResponse(ErrorCodes.TokenController.VerifyOldVersion, Resource.MessageTokenVerifyOldVersion); - } - catch (UserTokenBadFormatException) - { - return BadRequestWithCommonResponse(ErrorCodes.TokenController.VerifyBadFormat, Resource.MessageTokenVerifyBadFormat); - } - catch (UserTokenUserNotExistException) - { - return BadRequestWithCommonResponse(ErrorCodes.TokenController.VerifyUserNotExist, Resource.MessageTokenVerifyUserNotExist); + return BadRequestWithCommonResponse(ErrorCodes.TokenController.VerifyInvalid, Resource.MessageTokenVerifyInvalid); } } } diff --git a/BackEnd/Timeline/Controllers/UserAvatarController.cs b/BackEnd/Timeline/Controllers/UserAvatarController.cs index 5b8c5cdf..072ab621 100644 --- a/BackEnd/Timeline/Controllers/UserAvatarController.cs +++ b/BackEnd/Timeline/Controllers/UserAvatarController.cs @@ -61,7 +61,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task Put([FromRoute][Username] string username, [FromBody] ByteData body) { - if (!UserHasPermission(UserPermission.UserManagement) && GetUsername() != username) + if (!UserHasPermission(UserPermission.UserManagement) && !await CheckIsSelf(username)) { return ForbidWithCommonResponse(Resource.MessageForbidNotAdministratorOrOwner); } @@ -91,7 +91,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task Delete([FromRoute][Username] string username) { - if (!UserHasPermission(UserPermission.UserManagement) && User.Identity!.Name != username) + if (!UserHasPermission(UserPermission.UserManagement) && !await CheckIsSelf(username)) { return ForbidWithCommonResponse(Resource.MessageForbidNotAdministratorOrOwner); } diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs index 740bd0ed..95a99a03 100644 --- a/BackEnd/Timeline/Controllers/UserController.cs +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -7,6 +7,7 @@ using Timeline.Auth; using Timeline.Filters; using Timeline.Models.Http; using Timeline.Models.Validation; +using Timeline.Services; using Timeline.Services.Mapper; using Timeline.Services.User; @@ -103,7 +104,7 @@ namespace Timeline.Controllers } else { - if (GetUsername() != username) + if (!await CheckIsSelf(username)) return ForbidWithCommonResponse(Resource.MessageForbidNotAdministratorOrOwner); if (body.Username is not null) @@ -112,7 +113,7 @@ namespace Timeline.Controllers if (body.Password is not null) return ForbidWithCommonResponse(Resource.MessageForbidNotAdministrator); - var user = await _userService.ModifyUserAsync(GetUserId(), _mapper.AutoMapperMap(body)); + var user = await _userService.ModifyUserAsync(GetAuthUserId(), _mapper.AutoMapperMap(body)); return await _mapper.MapAsync(user, Url, User); } } @@ -152,7 +153,7 @@ namespace Timeline.Controllers { try { - await _userService.ChangePassword(GetUserId(), request.OldPassword, request.NewPassword); + await _userService.ChangePassword(GetAuthUserId(), request.OldPassword, request.NewPassword); return OkWithCommonResponse(); } catch (BadPasswordException) diff --git a/BackEnd/Timeline/Entities/UserTokenEntity.cs b/BackEnd/Timeline/Entities/UserTokenEntity.cs index 0d8bce7d..d5bae7f9 100644 --- a/BackEnd/Timeline/Entities/UserTokenEntity.cs +++ b/BackEnd/Timeline/Entities/UserTokenEntity.cs @@ -24,5 +24,8 @@ namespace Timeline.Entities [Column("create_at")] public DateTime? CreateAt { get; set; } + + [Column("deleted")] + public bool Deleted { get; set; } } } diff --git a/BackEnd/Timeline/ErrorCodes.cs b/BackEnd/Timeline/ErrorCodes.cs index be532c0c..9201979f 100644 --- a/BackEnd/Timeline/ErrorCodes.cs +++ b/BackEnd/Timeline/ErrorCodes.cs @@ -29,10 +29,7 @@ public static class Token { public const int TimeExpired = 1_000_21_01; - public const int VersionExpired = 1_000_21_02; - public const int BadFormat = 1_000_21_03; - public const int UserNotExist = 1_000_21_04; - public const int Unknown = 1_000_21_05; + public const int Invalid = 1_000_21_02; } } @@ -61,10 +58,8 @@ public static class TokenController { public const int CreateBadCredential = 1_101_01_01; - public const int VerifyBadFormat = 1_101_02_01; - public const int VerifyUserNotExist = 1_101_02_02; - public const int VerifyOldVersion = 1_101_02_03; - public const int VerifyTimeExpired = 1_101_02_04; + public const int VerifyInvalid = 1_101_02_02; + public const int VerifyExpired = 1_101_02_03; } public static class UserController diff --git a/BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.Designer.cs b/BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.Designer.cs new file mode 100644 index 00000000..89f2454c --- /dev/null +++ b/BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.Designer.cs @@ -0,0 +1,628 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +#nullable disable + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220323073853_AddDeletedToToken")] + partial class AddDeletedToToken + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.3"); + + modelBuilder.Entity("Timeline.Entities.BookmarkTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Rank") + .HasColumnType("INTEGER") + .HasColumnName("rank"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("bookmark_timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Ref") + .HasColumnType("INTEGER") + .HasColumnName("ref"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tag"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AddTime") + .HasColumnType("TEXT") + .HasColumnName("add_time"); + + b.Property("OperatorId") + .HasColumnType("INTEGER") + .HasColumnName("operator_id"); + + b.Property("Order") + .HasColumnType("INTEGER") + .HasColumnName("order"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline_id"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("highlight_timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("key"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.MigrationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("migrations"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Color") + .HasColumnType("TEXT") + .HasColumnName("color"); + + b.Property("CreateTime") + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("CurrentPostLocalId") + .HasColumnType("INTEGER") + .HasColumnName("current_post_local_id"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("NameLastModified") + .HasColumnType("TEXT") + .HasColumnName("name_last_modified"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("owner"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnType("INTEGER") + .HasColumnName("visibility"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostDataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DataTag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("data_tag"); + + b.Property("Index") + .HasColumnType("INTEGER") + .HasColumnName("index"); + + b.Property("Kind") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("kind"); + + b.Property("LastUpdated") + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("PostId") + .HasColumnType("INTEGER") + .HasColumnName("post"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.ToTable("timeline_post_data"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("INTEGER") + .HasColumnName("author"); + + b.Property("Color") + .HasColumnType("TEXT") + .HasColumnName("color"); + + b.Property("Content") + .HasColumnType("TEXT") + .HasColumnName("content"); + + b.Property("ContentType") + .HasColumnType("TEXT") + .HasColumnName("content_type"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("ExtraContent") + .HasColumnType("TEXT") + .HasColumnName("extra_content"); + + b.Property("LastUpdated") + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("LocalId") + .HasColumnType("INTEGER") + .HasColumnName("local_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DataTag") + .HasColumnType("TEXT") + .HasColumnName("data_tag"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("create_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("last_modified") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnType("TEXT") + .HasColumnName("nickname"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("username_change_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0L) + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("permission"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_permission"); + }); + + modelBuilder.Entity("Timeline.Entities.UserTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateAt") + .HasColumnType("TEXT") + .HasColumnName("create_at"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("ExpireAt") + .HasColumnType("TEXT") + .HasColumnName("expire_at"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("token"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("user_token"); + }); + + modelBuilder.Entity("Timeline.Entities.BookmarkTimelineEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Operator") + .WithMany() + .HasForeignKey("OperatorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Operator"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostDataEntity", b => + { + b.HasOne("Timeline.Entities.TimelinePostEntity", "Post") + .WithMany("DataList") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.UserTokenEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Posts"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Navigation("DataList"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Navigation("Avatar"); + + b.Navigation("Permissions"); + + b.Navigation("TimelinePosts"); + + b.Navigation("Timelines"); + + b.Navigation("TimelinesJoined"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.cs b/BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.cs new file mode 100644 index 00000000..f16d3522 --- /dev/null +++ b/BackEnd/Timeline/Migrations/20220323073853_AddDeletedToToken.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Timeline.Migrations +{ + public partial class AddDeletedToToken : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "deleted", + table: "user_token", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "deleted", + table: "user_token"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs index f1e9b6ab..f510d983 100644 --- a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -442,6 +442,10 @@ namespace Timeline.Migrations .HasColumnType("TEXT") .HasColumnName("create_at"); + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + b.Property("ExpireAt") .HasColumnType("TEXT") .HasColumnName("expire_at"); diff --git a/BackEnd/Timeline/Services/Token/DatabaseUserTokenHandler.cs b/BackEnd/Timeline/Services/Token/DatabaseUserTokenHandler.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/BackEnd/Timeline/Services/Token/IUserTokenHandler.cs b/BackEnd/Timeline/Services/Token/IUserTokenHandler.cs deleted file mode 100644 index 62e01de5..00000000 --- a/BackEnd/Timeline/Services/Token/IUserTokenHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Timeline.Services.Token -{ - public interface IUserTokenHandler - { - /// - /// Create a token for a given token info. - /// - /// The info to generate token. - /// Return the generated token. - /// Thrown when is null. - Task GenerateTokenAsync(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. - /// Do not check expire time in this method, only check whether it is present. - /// - Task ValidateTokenAsync(string token); - } -} diff --git a/BackEnd/Timeline/Services/Token/IUserTokenManager.cs b/BackEnd/Timeline/Services/Token/IUserTokenManager.cs deleted file mode 100644 index 39009d69..00000000 --- a/BackEnd/Timeline/Services/Token/IUserTokenManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Services.User; - -namespace Timeline.Services.Token -{ - 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 CreateTokenAsync(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 VerifyTokenAsync(string token); - } -} diff --git a/BackEnd/Timeline/Services/Token/IUserTokenService.cs b/BackEnd/Timeline/Services/Token/IUserTokenService.cs new file mode 100644 index 00000000..22fb0fb4 --- /dev/null +++ b/BackEnd/Timeline/Services/Token/IUserTokenService.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; + +namespace Timeline.Services.Token +{ + public interface IUserTokenService + { + /// + /// Create a token for a user. Please ensure the user id exists! + /// + /// The user id. + /// The expire time of the token. + /// Return the generated token. + Task CreateTokenAsync(long userId, DateTime? expireTime); + + /// + /// Verify a token and get the info of the token. + /// + /// The token to verify. + /// The info of the token. + /// Thrown when is null. + /// Thrown when the token is not valid for reasons other than expired. + /// Thrown when the token is expired. + Task ValidateTokenAsync(string token); + + /// + /// Revoke a token to make it no longer valid. + /// + /// The token to revoke. + /// Return true if a token is revoked. + /// Thrown when is null. + /// + /// This method returns true if a real token is revoked and returns false if the token is not valid. + /// If the token is expired, false is return. + /// + Task RevokeTokenAsync(string token); + + /// + /// Revoke all tokens of a user. + /// + /// User id of tokens. + /// Return the task. + Task RevokeAllTokenByUserIdAsync(long userId); + } +} diff --git a/BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs b/BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs deleted file mode 100644 index 7d272170..00000000 --- a/BackEnd/Timeline/Services/Token/JwtUserTokenBadFormatException.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Globalization; - -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 => Resource.ExceptionJwtUserTokenBadFormatReasonIdMissing, - ErrorKind.IdClaimBadFormat => Resource.ExceptionJwtUserTokenBadFormatReasonIdBadFormat, - ErrorKind.NoVersionClaim => Resource.ExceptionJwtUserTokenBadFormatReasonVersionMissing, - ErrorKind.VersionClaimBadFormat => Resource.ExceptionJwtUserTokenBadFormatReasonVersionBadFormat, - ErrorKind.Other => Resource.ExceptionJwtUserTokenBadFormatReasonOthers, - _ => Resource.ExceptionJwtUserTokenBadFormatReasonUnknown - }; - - return string.Format(CultureInfo.CurrentCulture, Resource.ExceptionJwtUserTokenBadFormat, reason); - } - } -} diff --git a/BackEnd/Timeline/Services/Token/Resource.Designer.cs b/BackEnd/Timeline/Services/Token/Resource.Designer.cs index ac6f3707..c1bd30ef 100644 --- a/BackEnd/Timeline/Services/Token/Resource.Designer.cs +++ b/BackEnd/Timeline/Services/Token/Resource.Designer.cs @@ -1,198 +1,78 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Timeline.Services.Token { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Services.Token.Resource", typeof(Resource).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Jwt key is not found. Maybe you forget to do the migration.. - /// - internal static string ExceptionJwtKeyNotExist { - get { - return ResourceManager.GetString("ExceptionJwtKeyNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token didn't pass verification because {0}.. - /// - internal static string ExceptionJwtUserTokenBadFormat { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to id claim is not a number. - /// - internal static string ExceptionJwtUserTokenBadFormatReasonIdBadFormat { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormatReasonIdBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to id claim does not exist. - /// - internal static string ExceptionJwtUserTokenBadFormatReasonIdMissing { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormatReasonIdMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to other error, see inner exception for information. - /// - internal static string ExceptionJwtUserTokenBadFormatReasonOthers { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormatReasonOthers", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to unknown error. - /// - internal static string ExceptionJwtUserTokenBadFormatReasonUnknown { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormatReasonUnknown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to version claim is not a number.. - /// - internal static string ExceptionJwtUserTokenBadFormatReasonVersionBadFormat { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormatReasonVersionBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to version claim does not exist.. - /// - internal static string ExceptionJwtUserTokenBadFormatReasonVersionMissing { - get { - return ResourceManager.GetString("ExceptionJwtUserTokenBadFormatReasonVersionMissing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad format, which means it may not be created by the server.. - /// - internal static string ExceptionUserTokenBadFormat { - get { - return ResourceManager.GetString("ExceptionUserTokenBadFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is expired because its expiration time has passed.. - /// - internal static string ExceptionUserTokenTimeExpired { - get { - return ResourceManager.GetString("ExceptionUserTokenTimeExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The owner user of the token does not exist.. - /// - internal static string ExceptionUserTokenUserNotExist { - get { - return ResourceManager.GetString("ExceptionUserTokenUserNotExist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The token is of bad version.. - /// - internal static string ExceptionUserTokenVersionExpired { - get { - return ResourceManager.GetString("ExceptionUserTokenVersionExpired", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token is created for user with username={0}, id={1}.. - /// - internal static string LogTokenCreate { - get { - return ResourceManager.GetString("LogTokenCreate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token of user with username = {0}, id = {1} is verified successfully.. - /// - internal static string LogTokenVerified { - get { - return ResourceManager.GetString("LogTokenVerified", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A token fails to be verified.. - /// - internal static string LogTokenVerifiedFail { - get { - return ResourceManager.GetString("LogTokenVerifiedFail", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Services.Token { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// This class was generated by MSBuild using the GenerateResource task. + /// To add or remove a member, edit your .resx file then rerun MSBuild. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Services.Token.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The token is expired because its expiration time has passed.. + /// + internal static string ExceptionUserTokenExpired { + get { + return ResourceManager.GetString("ExceptionUserTokenExpired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The token is invalid.. + /// + internal static string ExceptionUserTokenInvalid { + get { + return ResourceManager.GetString("ExceptionUserTokenInvalid", resourceCulture); + } + } + } +} diff --git a/BackEnd/Timeline/Services/Token/Resource.resx b/BackEnd/Timeline/Services/Token/Resource.resx index 06bf03f6..9ea2e63a 100644 --- a/BackEnd/Timeline/Services/Token/Resource.resx +++ b/BackEnd/Timeline/Services/Token/Resource.resx @@ -1,6 +1,6 @@  - - - - - - - + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Jwt key is not found. Maybe you forget to do the migration. - - - The token didn't pass verification because {0}. - - - id claim is not a number - - - id claim does not exist - - - other error, see inner exception for information - - - unknown error - - - version claim is not a number. - - - version claim does not exist. - - - The token is of bad format, which means it may not be created by the server. - - - The token is expired because its expiration time has passed. - - - The owner user of the token does not exist. - - - The token is of bad version. - - - A token is created for user with username={0}, id={1}. - - - A token of user with username = {0}, id = {1} is verified successfully. - - - A token fails to be verified. - + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The token is invalid. + + + The token is expired because its expiration time has passed. + \ No newline at end of file diff --git a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs new file mode 100644 index 00000000..404862d4 --- /dev/null +++ b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Timeline.Configs; +using Timeline.Entities; + +namespace Timeline.Services.Token +{ + public class SecureRandomUserTokenService : IUserTokenService, IDisposable + { + private DatabaseContext _databaseContext; + private ILogger _logger; + private RandomNumberGenerator _secureRandom; + private IOptionsMonitor _optionMonitor; + private IClock _clock; + + public SecureRandomUserTokenService(DatabaseContext databaseContext, ILogger logger, IOptionsMonitor optionMonitor, IClock clock) + { + _databaseContext = databaseContext; + _logger = logger; + _secureRandom = RandomNumberGenerator.Create(); + _optionMonitor = optionMonitor; + _clock = clock; + } + + public void Dispose() + { + _secureRandom.Dispose(); + } + + private string GenerateSecureRandomTokenString() + { + var option = _optionMonitor.CurrentValue; + var tokenLength = option.TokenLength ?? 32; + var buffer = new byte[tokenLength]; + _secureRandom.GetBytes(buffer); + return Convert.ToHexString(buffer); + } + + /// + public async Task CreateTokenAsync(long userId, DateTime? expireTime) + { + var currentTime = _clock.GetCurrentTime(); + + if (expireTime is not null && expireTime > currentTime) + { + _logger.LogWarning("The expire time of the token has already passed."); + } + + UserTokenEntity entity = new UserTokenEntity + { + UserId = userId, + Token = GenerateSecureRandomTokenString(), + ExpireAt = expireTime, + CreateAt = currentTime, + Deleted = false + }; + + _databaseContext.UserTokens.Add(entity); + await _databaseContext.SaveChangesAsync(); + + _logger.LogInformation("A user token is created with user id {}.", userId); + + return entity.Token; + } + + /// + public async Task ValidateTokenAsync(string token) + { + var entity = await _databaseContext.UserTokens.Where(t => t.Token == token && !t.Deleted).SingleOrDefaultAsync(); + + if (entity is null) + { + throw new UserTokenException(token, Resource.ExceptionUserTokenInvalid); + } + + var currentTime = _clock.GetCurrentTime(); + + if (entity.ExpireAt.HasValue && entity.ExpireAt > currentTime) + { + throw new UserTokenExpiredException(token, entity.ExpireAt.Value, currentTime); + } + + return new UserTokenInfo() + { + UserId = entity.UserId, + ExpireAt = entity.ExpireAt, + CreateAt = entity.CreateAt + }; + } + + /// + public async Task RevokeTokenAsync(string token) + { + var entity = await _databaseContext.UserTokens.Where(t => t.Token == token && t.Deleted == false).SingleOrDefaultAsync(); + if (entity is not null) + { + entity.Deleted = true; + await _databaseContext.SaveChangesAsync(); + + _logger.LogInformation("A token is revoked with user id {}.", entity.UserId); + + return entity.ExpireAt <= _clock.GetCurrentTime(); + } + return false; + } + + /// + public async Task RevokeAllTokenByUserIdAsync(long userId) + { + List entities = await _databaseContext.UserTokens.Where(t => t.UserId == userId && t.Deleted == false).ToListAsync(); + foreach (var entity in entities) + { + entity.Deleted = true; + } + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation("All tokens of user with id {} are revoked.", userId); + } + } +} \ No newline at end of file diff --git a/BackEnd/Timeline/Services/Token/TokenServicesServiceColletionExtensions.cs b/BackEnd/Timeline/Services/Token/TokenServicesServiceColletionExtensions.cs index 1ad84311..cf4eeb11 100644 --- a/BackEnd/Timeline/Services/Token/TokenServicesServiceColletionExtensions.cs +++ b/BackEnd/Timeline/Services/Token/TokenServicesServiceColletionExtensions.cs @@ -9,9 +9,7 @@ namespace Timeline.Services.Token public static IServiceCollection AddTokenServices(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("Token")); - services.Configure(configuration.GetSection("Jwt")); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/BackEnd/Timeline/Services/Token/UserTokenBadFormatException.cs b/BackEnd/Timeline/Services/Token/UserTokenBadFormatException.cs deleted file mode 100644 index 39ed1be4..00000000 --- a/BackEnd/Timeline/Services/Token/UserTokenBadFormatException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Timeline.Services.Token -{ - [Serializable] - public class UserTokenBadFormatException : UserTokenException - { - public UserTokenBadFormatException() : base(Resource.ExceptionUserTokenBadFormat) { } - public UserTokenBadFormatException(string token) : base(token, Resource.ExceptionUserTokenBadFormat) { } - public UserTokenBadFormatException(string token, string message) : base(token, message) { } - public UserTokenBadFormatException(string token, Exception inner) : base(token, Resource.ExceptionUserTokenBadFormat, 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/UserTokenExpiredException.cs b/BackEnd/Timeline/Services/Token/UserTokenExpiredException.cs new file mode 100644 index 00000000..5e91ca6c --- /dev/null +++ b/BackEnd/Timeline/Services/Token/UserTokenExpiredException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Timeline.Services.Token +{ + [Serializable] + public class UserTokenExpiredException : UserTokenException + { + public UserTokenExpiredException() : base(Resource.ExceptionUserTokenExpired) { } + public UserTokenExpiredException(string message) : base(message) { } + public UserTokenExpiredException(string message, Exception inner) : base(message, inner) { } + public UserTokenExpiredException(string token, DateTime expireTime, DateTime verifyTime) : base(token, Resource.ExceptionUserTokenExpired) { ExpireTime = expireTime; VerifyTime = verifyTime; } + public UserTokenExpiredException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resource.ExceptionUserTokenExpired, inner) { ExpireTime = expireTime; VerifyTime = verifyTime; } + protected UserTokenExpiredException( + 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; } + } +} diff --git a/BackEnd/Timeline/Services/Token/UserTokenHandler.cs b/BackEnd/Timeline/Services/Token/UserTokenHandler.cs deleted file mode 100644 index 03b07b53..00000000 --- a/BackEnd/Timeline/Services/Token/UserTokenHandler.cs +++ /dev/null @@ -1,117 +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 System.Threading.Tasks; -using Timeline.Configs; -using Timeline.Entities; - -namespace Timeline.Services.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(Resource.ExceptionJwtKeyNotExist); - } - - _tokenSecurityKey = new SymmetricSecurityKey(key); - } - - public Task GenerateTokenAsync(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 Task.FromResult(tokenString); - } - - - public Task ValidateTokenAsync(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 Task.FromResult(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/UserTokenInfo.cs b/BackEnd/Timeline/Services/Token/UserTokenInfo.cs index 547f5ba6..b1a386d1 100644 --- a/BackEnd/Timeline/Services/Token/UserTokenInfo.cs +++ b/BackEnd/Timeline/Services/Token/UserTokenInfo.cs @@ -4,8 +4,8 @@ namespace Timeline.Services.Token { public class UserTokenInfo { - public long Id { get; set; } - public long Version { get; set; } - public DateTime ExpireAt { get; set; } + public long UserId { get; set; } + public DateTime? ExpireAt { get; set; } + public DateTime? CreateAt { get; set; } } } diff --git a/BackEnd/Timeline/Services/Token/UserTokenManager.cs b/BackEnd/Timeline/Services/Token/UserTokenManager.cs deleted file mode 100644 index bdb229f0..00000000 --- a/BackEnd/Timeline/Services/Token/UserTokenManager.cs +++ /dev/null @@ -1,102 +0,0 @@ -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.User; - -namespace Timeline.Services.Token -{ - public class UserTokenManager : IUserTokenManager - { - private readonly ILogger _logger; - private readonly IOptionsMonitor _tokenOptionsMonitor; - private readonly IUserService _userService; - private readonly IUserTokenHandler _userTokenService; - private readonly IClock _clock; - - public UserTokenManager(ILogger logger, IOptionsMonitor tokenOptionsMonitor, IUserService userService, IUserTokenHandler userTokenService, IClock clock) - { - _logger = logger; - _tokenOptionsMonitor = tokenOptionsMonitor; - _userService = userService; - _userTokenService = userTokenService; - _clock = clock; - } - - public async Task CreateTokenAsync(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 _userService.VerifyCredential(username, password); - var user = await _userService.GetUserAsync(userId); - - var token = await _userTokenService.GenerateTokenAsync(new UserTokenInfo - { - Id = user.Id, - Version = user.Version, - ExpireAt = expireAt ?? _clock.GetCurrentTime() + TimeSpan.FromSeconds(_tokenOptionsMonitor.CurrentValue.DefaultExpireSeconds) - }); - - _logger.LogInformation(Resource.LogTokenCreate, user.Username, userId); - - return new UserTokenCreateResult { Token = token, User = user }; - } - - - public async Task VerifyTokenAsync(string token) - { - if (token == null) - throw new ArgumentNullException(nameof(token)); - - UserTokenInfo tokenInfo; - - try - { - tokenInfo = await _userTokenService.ValidateTokenAsync(token); - } - catch (UserTokenBadFormatException e) - { - _logger.LogInformation(e, Resource.LogTokenVerifiedFail); - throw; - } - - var currentTime = _clock.GetCurrentTime(); - if (tokenInfo.ExpireAt < currentTime) - { - var e = new UserTokenTimeExpiredException(token, tokenInfo.ExpireAt, currentTime); - _logger.LogInformation(e, Resource.LogTokenVerifiedFail); - throw e; - } - - try - { - var user = await _userService.GetUserAsync(tokenInfo.Id); - - if (tokenInfo.Version < user.Version) - { - var e = new UserTokenVersionExpiredException(token, tokenInfo.Version, user.Version); - _logger.LogInformation(e, Resource.LogTokenVerifiedFail); - throw e; - } - - _logger.LogInformation(Resource.LogTokenVerified, user.Username, user.Id); - - return user; - } - catch (EntityNotExistException e) - { - var exception = new UserTokenUserNotExistException(token, e); - _logger.LogInformation(exception, Resource.LogTokenVerifiedFail); - throw exception; - } - } - } -} diff --git a/BackEnd/Timeline/Services/Token/UserTokenTimeExpiredException.cs b/BackEnd/Timeline/Services/Token/UserTokenTimeExpiredException.cs deleted file mode 100644 index 6e33ab4d..00000000 --- a/BackEnd/Timeline/Services/Token/UserTokenTimeExpiredException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services.Token -{ - [Serializable] - public class UserTokenTimeExpiredException : UserTokenException - { - public UserTokenTimeExpiredException() : base(Resource.ExceptionUserTokenTimeExpired) { } - 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, Resource.ExceptionUserTokenTimeExpired) { ExpireTime = expireTime; VerifyTime = verifyTime; } - public UserTokenTimeExpiredException(string token, DateTime expireTime, DateTime verifyTime, Exception inner) : base(token, Resource.ExceptionUserTokenTimeExpired, 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; } - } -} diff --git a/BackEnd/Timeline/Services/Token/UserTokenUserNotExistException.cs b/BackEnd/Timeline/Services/Token/UserTokenUserNotExistException.cs deleted file mode 100644 index 28f56938..00000000 --- a/BackEnd/Timeline/Services/Token/UserTokenUserNotExistException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Timeline.Services.Token -{ - [Serializable] - public class UserTokenUserNotExistException : UserTokenException - { - public UserTokenUserNotExistException() : base(Resource.ExceptionUserTokenUserNotExist) { } - public UserTokenUserNotExistException(string token) : base(token, Resource.ExceptionUserTokenUserNotExist) { } - public UserTokenUserNotExistException(string token, Exception inner) : base(token, Resource.ExceptionUserTokenUserNotExist, inner) { } - - protected UserTokenUserNotExistException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } -} diff --git a/BackEnd/Timeline/Services/Token/UserTokenVersionExpiredException.cs b/BackEnd/Timeline/Services/Token/UserTokenVersionExpiredException.cs deleted file mode 100644 index db6b4669..00000000 --- a/BackEnd/Timeline/Services/Token/UserTokenVersionExpiredException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Timeline.Services.Token -{ - [Serializable] - public class UserTokenVersionExpiredException : UserTokenException - { - public UserTokenVersionExpiredException() : base(Resource.ExceptionUserTokenVersionExpired) { } - 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, Resource.ExceptionUserTokenVersionExpired) { TokenVersion = tokenVersion; RequiredVersion = requiredVersion; } - public UserTokenVersionExpiredException(string token, long tokenVersion, long requiredVersion, Exception inner) : base(token, Resource.ExceptionUserTokenVersionExpired, 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; } - } -} diff --git a/BackEnd/Timeline/Services/User/CreateTokenResult.cs b/BackEnd/Timeline/Services/User/CreateTokenResult.cs new file mode 100644 index 00000000..b71a9e9e --- /dev/null +++ b/BackEnd/Timeline/Services/User/CreateTokenResult.cs @@ -0,0 +1,12 @@ +using System; +using Timeline.Entities; + +namespace Timeline.Services.User +{ + public class CreateTokenResult + { + public string Token { get; set; } = default!; + public UserEntity User { get; set; } = default!; + } +} + diff --git a/BackEnd/Timeline/Services/User/UserService.cs b/BackEnd/Timeline/Services/User/UserService.cs index a47bc860..1ad74bec 100644 --- a/BackEnd/Timeline/Services/User/UserService.cs +++ b/BackEnd/Timeline/Services/User/UserService.cs @@ -7,7 +7,8 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models.Validation; - +using Timeline.Services.Token; + namespace Timeline.Services.User { public class UserService : BasicUserService, IUserService @@ -19,14 +20,17 @@ namespace Timeline.Services.User private readonly IPasswordService _passwordService; + private readonly IUserTokenService _userTokenService; + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock) : base(databaseContext) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IUserTokenService userTokenService, IClock clock) : base(databaseContext) { _logger = logger; _databaseContext = databaseContext; _passwordService = passwordService; + _userTokenService = userTokenService; _clock = clock; } @@ -162,6 +166,11 @@ namespace Timeline.Services.User await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Resource.LogUserModified, entity.Username, id); + + if (password is not null) + { + await _userTokenService.RevokeAllTokenByUserIdAsync(id); + } } return entity; @@ -214,6 +223,8 @@ namespace Timeline.Services.User entity.Version += 1; await _databaseContext.SaveChangesAsync(); _logger.LogInformation(Resource.LogChangePassowrd, entity.Username, id); + + await _userTokenService.RevokeAllTokenByUserIdAsync(id); } } } diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj index 95376b9c..48cd7f10 100644 --- a/BackEnd/Timeline/Timeline.csproj +++ b/BackEnd/Timeline/Timeline.csproj @@ -18,6 +18,9 @@ 1591 + + + PreserveNewest diff --git a/BackEnd/Timeline/appsettings.json b/BackEnd/Timeline/appsettings.json index 81c01bf6..0804371b 100644 --- a/BackEnd/Timeline/appsettings.json +++ b/BackEnd/Timeline/appsettings.json @@ -3,9 +3,5 @@ "LogLevel": { "Default": "Warning" } - }, - "Jwt": { - "Issuer": "crupest.space", - "Audience": "crupest.space" } } -- cgit v1.2.3