From eb50fc7ad1252d0e397e969e1a7eb33720150b00 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 20 Jul 2019 19:01:35 +0800 Subject: Write a custom authenticate handler. --- Timeline/Authenticate/AuthHandler.cs | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Timeline/Authenticate/AuthHandler.cs (limited to 'Timeline/Authenticate') diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs new file mode 100644 index 00000000..71d8aeaa --- /dev/null +++ b/Timeline/Authenticate/AuthHandler.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Timeline.Authenticate +{ + static class AuthConstants + { + public const string Scheme = "Bearer"; + public const string DisplayName = "My Jwt Auth Scheme"; + } + + class AuthOptions : AuthenticationSchemeOptions + { + /// + /// The query param key to search for token. If null then query params are not searched for token. Default to "token". + /// + public string TokenQueryParamKey { get; set; } = "token"; + + public TokenValidationParameters TokenValidationParameters { get; + set; } + = new TokenValidationParameters(); + } + + class AuthHandler : AuthenticationHandler + { + private readonly ILogger _logger; + + public AuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : base(options, logger, encoder, clock) + { + _logger = logger.CreateLogger(); + } + + // return null if no token is found + private string ExtractToken() + { + // check the authorization header + string header = Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + var token = header.Substring("Bearer ".Length).Trim(); + _logger.LogInformation("Token is found in authorization header. Token is {} .", token); + return token; + } + + // check the query params + var paramQueryKey = Options.TokenQueryParamKey; + if (!string.IsNullOrEmpty(paramQueryKey)) + { + string token = Request.Query[paramQueryKey]; + if (!string.IsNullOrEmpty(token)) + { + _logger.LogInformation("Token is found in query param with key \"{}\". Token is {} .", paramQueryKey, token); + return token; + } + } + + // not found anywhere then return null + return null; + } + + protected override async Task HandleAuthenticateAsync() + { + var token = ExtractToken(); + if (string.IsNullOrEmpty(token)) + { + _logger.LogInformation("No jwt token is found."); + return AuthenticateResult.NoResult(); + } + + var handler = new JwtSecurityTokenHandler(); + try + { + var principal = handler.ValidateToken(token, Options.TokenValidationParameters, out var validatedToken); + return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme)); + } + catch (SecurityTokenException e) + { + _logger.LogInformation(e, "A jwt token validation failed."); + return AuthenticateResult.Fail(e); + } + catch (Exception e) + { + _logger.LogError(e, "Arguments passed to the JwtSecurityTokenHandler.ValidateToken are bad."); + throw e; + } + } + } +} -- cgit v1.2.3 From 615ffca61fcc90b11b04c8d115018a26a4a63a33 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 20 Jul 2019 23:47:41 +0800 Subject: WIP: Change the JwtService. --- Timeline/Authenticate/AuthHandler.cs | 4 +-- Timeline/Configs/JwtConfig.cs | 6 ++++ Timeline/Services/JwtService.cs | 62 ++++++++++++++++++++++++------------ 3 files changed, 49 insertions(+), 23 deletions(-) (limited to 'Timeline/Authenticate') diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs index 71d8aeaa..63442481 100644 --- a/Timeline/Authenticate/AuthHandler.cs +++ b/Timeline/Authenticate/AuthHandler.cs @@ -23,9 +23,7 @@ namespace Timeline.Authenticate /// public string TokenQueryParamKey { get; set; } = "token"; - public TokenValidationParameters TokenValidationParameters { get; - set; } - = new TokenValidationParameters(); + public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); } class AuthHandler : AuthenticationHandler diff --git a/Timeline/Configs/JwtConfig.cs b/Timeline/Configs/JwtConfig.cs index 9550424e..1b395650 100644 --- a/Timeline/Configs/JwtConfig.cs +++ b/Timeline/Configs/JwtConfig.cs @@ -5,5 +5,11 @@ namespace Timeline.Configs public string Issuer { get; set; } public string Audience { get; set; } public string SigningKey { get; set; } + + /// + /// Set the default value of expire offset of jwt token. + /// Unit is second. Default is 3600 seconds, aka 1 hour. + /// + public long DefaultExpireOffset { get; set; } = 3600; } } diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index 2139ba56..e7f5690d 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Security.Claims; using System.Text; using Timeline.Configs; @@ -13,30 +12,45 @@ namespace Timeline.Services public class TokenInfo { public string Name { get; set; } - public string[] Roles { get; set; } + public long Version { get; set; } + } + + [Serializable] + public class JwtTokenVerifyException : Exception + { + public JwtTokenVerifyException() { } + public JwtTokenVerifyException(string message) : base(message) { } + public JwtTokenVerifyException(string message, Exception inner) : base(message, inner) { } + protected JwtTokenVerifyException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } } public interface IJwtService { /// - /// Create a JWT token for a given user info. + /// Create a JWT token for a given token info. /// /// The info to generate token. + /// The expire time. If null then use current time with offset in config. /// Return the generated token. - string GenerateJwtToken(TokenInfo tokenInfo); + string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null); /// /// Verify a JWT token. /// Return null is is null. /// /// The token string to verify. - /// Return null if is null or token is invalid. Return the saved info otherwise. + /// Return null if is null. Return the saved info otherwise. + /// Thrown when the token is invalid. TokenInfo VerifyJwtToken(string token); } public class JwtService : IJwtService { + private const string VersionClaimType = "timeline_version"; + private readonly IOptionsMonitor _jwtConfig; private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); private readonly ILogger _logger; @@ -47,30 +61,28 @@ namespace Timeline.Services _logger = logger; } - public string GenerateJwtToken(TokenInfo tokenInfo) + public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null) { if (tokenInfo == null) throw new ArgumentNullException(nameof(tokenInfo)); if (tokenInfo.Name == null) - throw new ArgumentException("Name is null.", nameof(tokenInfo)); - if (tokenInfo.Roles == null) - throw new ArgumentException("Roles is null.", nameof(tokenInfo)); + throw new ArgumentException("Name of token info is null.", nameof(tokenInfo)); - var jwtConfig = _jwtConfig.CurrentValue; + var config = _jwtConfig.CurrentValue; var identity = new ClaimsIdentity(); identity.AddClaim(new Claim(identity.NameClaimType, tokenInfo.Name)); - identity.AddClaims(tokenInfo.Roles.Select(role => new Claim(identity.RoleClaimType, role))); + identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64)); var tokenDescriptor = new SecurityTokenDescriptor() { Subject = identity, - Issuer = jwtConfig.Issuer, - Audience = jwtConfig.Audience, + Issuer = config.Issuer, + Audience = config.Audience, SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SigningKey)), SecurityAlgorithms.HmacSha384), + new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384), IssuedAt = DateTime.Now, - Expires = DateTime.Now.AddDays(1) + Expires = expires.GetValueOrDefault(DateTime.Now.AddSeconds(config.DefaultExpireOffset)) }; var token = _tokenHandler.CreateToken(tokenDescriptor); @@ -97,18 +109,28 @@ namespace Timeline.Services ValidIssuer = config.Issuer, ValidAudience = config.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)) - }, out SecurityToken validatedToken); + }, out _); + + var versionClaim = principal.FindFirstValue(VersionClaimType); + if (versionClaim == null) + throw new JwtTokenVerifyException("Version claim does not exist."); + if (!long.TryParse(versionClaim, out var version)) + throw new JwtTokenVerifyException("Can't convert version claim into a integer number."); return new TokenInfo { Name = principal.Identity.Name, - Roles = principal.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray() + Version = version }; } - catch (Exception e) + catch (SecurityTokenException e) { - _logger.LogInformation(e, "Token validation failed! Token is {} .", token); - return null; + throw new JwtTokenVerifyException("Validate token failed caused by a SecurityTokenException. See inner exception.", e); + } + catch (ArgumentException e) // This usually means code logic error. + { + _logger.LogError(e, "Arguments passed to ValidateToken are bad."); + throw e; } } } -- cgit v1.2.3 From 35300c1b1ce6377393d4c9353416daae23b6d17c Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 21 Jul 2019 23:28:21 +0800 Subject: WIP: change auth handler. --- Timeline/Authenticate/AuthHandler.cs | 29 ++++++++++++++++------------- Timeline/Startup.cs | 13 +------------ 2 files changed, 17 insertions(+), 25 deletions(-) (limited to 'Timeline/Authenticate') diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs index 63442481..80bbaf14 100644 --- a/Timeline/Authenticate/AuthHandler.cs +++ b/Timeline/Authenticate/AuthHandler.cs @@ -1,12 +1,13 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; using Microsoft.Net.Http.Headers; using System; -using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Timeline.Services; namespace Timeline.Authenticate { @@ -22,18 +23,18 @@ namespace Timeline.Authenticate /// The query param key to search for token. If null then query params are not searched for token. Default to "token". /// public string TokenQueryParamKey { get; set; } = "token"; - - public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); } class AuthHandler : AuthenticationHandler { private readonly ILogger _logger; + private readonly IUserService _userService; - public AuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + public AuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) : base(options, logger, encoder, clock) { _logger = logger.CreateLogger(); + _userService = userService; } // return null if no token is found @@ -73,22 +74,24 @@ namespace Timeline.Authenticate return AuthenticateResult.NoResult(); } - var handler = new JwtSecurityTokenHandler(); try { - var principal = handler.ValidateToken(token, Options.TokenValidationParameters, out var validatedToken); + var userInfo = await _userService.VerifyToken(token); + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); + identity.AddClaims(Entities.UserUtility.IsAdminToRoleArray(userInfo.IsAdmin).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + + var principal = new ClaimsPrincipal(); + principal.AddIdentity(identity); + return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme)); } - catch (SecurityTokenException e) + catch (Exception e) { _logger.LogInformation(e, "A jwt token validation failed."); return AuthenticateResult.Fail(e); } - catch (Exception e) - { - _logger.LogError(e, "Arguments passed to the JwtSecurityTokenHandler.ValidateToken are bad."); - throw e; - } } } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 83170c43..374b918a 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -7,8 +7,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.IdentityModel.Tokens; -using System.Text; using Timeline.Authenticate; using Timeline.Configs; using Timeline.Formatters; @@ -53,16 +51,7 @@ namespace Timeline var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddScheme(AuthConstants.Scheme, AuthConstants.DisplayName, o => - { - o.TokenValidationParameters.ValidateIssuer = true; - o.TokenValidationParameters.ValidateAudience = true; - o.TokenValidationParameters.ValidateIssuerSigningKey = true; - o.TokenValidationParameters.ValidateLifetime = true; - o.TokenValidationParameters.ValidIssuer = jwtConfig.Issuer; - o.TokenValidationParameters.ValidAudience = jwtConfig.Audience; - o.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SigningKey)); - }); + .AddScheme(AuthConstants.Scheme, AuthConstants.DisplayName, o => { }); services.AddScoped(); services.AddScoped(); -- cgit v1.2.3 From e34873c8cec905cd38c754d8968e848c9d0db5c2 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 23 Jul 2019 18:01:04 +0800 Subject: WIP: Change UserController. --- Timeline/Authenticate/Attribute.cs | 21 ++++++ Timeline/Controllers/TokenController.cs | 30 ++++---- Timeline/Controllers/UserController.cs | 130 +++++++++++++++----------------- Timeline/Entities/Http/Common.cs | 24 +++++- Timeline/Entities/Http/User.cs | 40 ---------- Timeline/Entities/UserUtility.cs | 4 +- Timeline/Models/DatabaseContext.cs | 6 ++ 7 files changed, 124 insertions(+), 131 deletions(-) create mode 100644 Timeline/Authenticate/Attribute.cs (limited to 'Timeline/Authenticate') diff --git a/Timeline/Authenticate/Attribute.cs b/Timeline/Authenticate/Attribute.cs new file mode 100644 index 00000000..50b2681d --- /dev/null +++ b/Timeline/Authenticate/Attribute.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; +using Timeline.Models; + +namespace Timeline.Authenticate +{ + public class AdminAuthorizeAttribute : AuthorizeAttribute + { + public AdminAuthorizeAttribute() + { + Roles = UserRoles.Admin; + } + } + + public class UserAuthorizeAttribute : AuthorizeAttribute + { + public UserAuthorizeAttribute() + { + Roles = UserRoles.User; + } + } +} diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs index 3a364ffe..023bd53f 100644 --- a/Timeline/Controllers/TokenController.cs +++ b/Timeline/Controllers/TokenController.cs @@ -21,12 +21,12 @@ namespace Timeline.Controllers private static class ErrorCodes { - public const int Create_UserNotExist = 1001; - public const int Create_BadPassword = 1002; + public const int Create_UserNotExist = -1001; + public const int Create_BadPassword = -1002; - public const int Verify_BadToken = 2001; - public const int Verify_UserNotExist = 2002; - public const int Verify_BadVersion = 2003; + public const int Verify_BadToken = -2001; + public const int Verify_UserNotExist = -2002; + public const int Verify_BadVersion = -2003; } private readonly IUserService _userService; @@ -55,14 +55,14 @@ namespace Timeline.Controllers catch(UserNotExistException e) { var code = ErrorCodes.Create_UserNotExist; - _logger.LogInformation(LoggingEventIds.LogInFailed, e, "Attemp to login failed. Code: {} Username: {} Password: {} .", code, request.Username, request.Password); - return BadRequest(new CommonErrorResponse(code, "Bad username or password.")); + _logger.LogInformation(LoggingEventIds.LogInFailed, e, "Attemp to login failed because user does not exist. Code: {} Username: {} Password: {} .", code, request.Username, request.Password); + return BadRequest(new CommonResponse(code, "Bad username or password.")); } catch (BadPasswordException e) { var code = ErrorCodes.Create_BadPassword; - _logger.LogInformation(LoggingEventIds.LogInFailed, e, "Attemp to login failed. Code: {} Username: {} Password: {} .", code, request.Username, request.Password); - return BadRequest(new CommonErrorResponse(code, "Bad username or password.")); + _logger.LogInformation(LoggingEventIds.LogInFailed, e, "Attemp to login failed because password is wrong. Code: {} Username: {} Password: {} .", code, request.Username, request.Password); + return BadRequest(new CommonResponse(code, "Bad username or password.")); } } @@ -82,20 +82,20 @@ namespace Timeline.Controllers catch (JwtTokenVerifyException e) { var code = ErrorCodes.Verify_BadToken; - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonErrorResponse(code, "A token of bad format.")); + _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because of bad format. Code: {} Token: {}.", code, request.Token); + return BadRequest(new CommonResponse(code, "A token of bad format.")); } catch (UserNotExistException e) { var code = ErrorCodes.Verify_UserNotExist; - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonErrorResponse(code, "The user does not exist. Administrator might have deleted this user.")); + _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because user does not exist. Code: {} Token: {}.", code, request.Token); + return BadRequest(new CommonResponse(code, "The user does not exist. Administrator might have deleted this user.")); } catch (BadTokenVersionException e) { var code = ErrorCodes.Verify_BadToken; - _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token. Code: {} Token: {}.", code, request.Token); - return BadRequest(new CommonErrorResponse(code, "The token is expired. Try recreate a token.")); + _logger.LogInformation(LoggingEventIds.VerifyFailed, e, "Attemp to verify a bad token because version is old. Code: {} Token: {}.", code, request.Token); + return BadRequest(new CommonResponse(code, "The token is expired. Try recreate a token.")); } } } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 6f708e8a..413999ce 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using System; -using System.IO; using System.Threading.Tasks; +using Timeline.Authenticate; using Timeline.Entities; using Timeline.Entities.Http; using Timeline.Services; @@ -12,125 +12,113 @@ namespace Timeline.Controllers { public class UserController : Controller { + private static class ErrorCodes + { + public const int Get_NotExists = -1001; + + public const int Put_NoPassword = -2001; + + public const int Patch_NotExists = -3001; + + public const int ChangePassword_BadOldPassword = -4001; + } + + private readonly ILogger _logger; private readonly IUserService _userService; - public UserController(IUserService userService) + public UserController(ILogger logger, IUserService userService) { + _logger = logger; _userService = userService; } - [HttpGet("users"), Authorize(Roles = "admin")] + [HttpGet("users"), AdminAuthorize] public async Task> List() { return Ok(await _userService.ListUsers()); } - [HttpGet("user/{username}"), Authorize] + [HttpGet("user/{username}"), AdminAuthorize] public async Task Get([FromRoute] string username) { var user = await _userService.GetUser(username); if (user == null) { - return NotFound(); + _logger.LogInformation("Attempt to get a non-existent user. Username: {} .", username); + return NotFound(new CommonResponse(ErrorCodes.Get_NotExists, "The user does not exist.")); } return Ok(user); } - [HttpPut("user/{username}"), Authorize(Roles = "admin")] + [HttpPut("user/{username}"), AdminAuthorize] public async Task Put([FromBody] UserPutRequest request, [FromRoute] string username) { + if (request.Password == null) + { + _logger.LogInformation("Attempt to put a user without a password. Username: {} .", username); + return BadRequest(); + } + var result = await _userService.PutUser(username, request.Password, request.IsAdmin); switch (result) { - case PutUserResult.Created: - return CreatedAtAction("Get", new { username }, UserPutResponse.Created); - case PutUserResult.Modified: - return Ok(UserPutResponse.Modified); + case PutResult.Created: + _logger.LogInformation("Created a user. Username: {} .", username); + return CreatedAtAction("Get", new { username }, CommonPutResponse.Created); + case PutResult.Modified: + _logger.LogInformation("Modified a user. Username: {} .", username); + return Ok(CommonPutResponse.Modified); default: throw new Exception("Unreachable code."); } } - [HttpPatch("user/{username}"), Authorize(Roles = "admin")] + [HttpPatch("user/{username}"), AdminAuthorize] public async Task Patch([FromBody] UserPatchRequest request, [FromRoute] string username) { - var result = await _userService.PatchUser(username, request.Password, request.IsAdmin); - switch (result) + try { - case PatchUserResult.Success: - return Ok(); - case PatchUserResult.NotExists: - return NotFound(); - default: - throw new Exception("Unreachable code."); + await _userService.PatchUser(username, request.Password, request.IsAdmin); + return Ok(); } - } - - [HttpDelete("user/{username}"), Authorize(Roles = "admin")] - public async Task Delete([FromRoute] string username) - { - var result = await _userService.DeleteUser(username); - switch (result) + catch (UserNotExistException e) { - case DeleteUserResult.Deleted: - return Ok(UserDeleteResponse.Deleted); - case DeleteUserResult.NotExists: - return Ok(UserDeleteResponse.NotExists); - default: - throw new Exception("Uncreachable code."); + _logger.LogInformation(e, "Attempt to patch a non-existent user. Username: {} .", username); + return BadRequest(new CommonResponse(ErrorCodes.Patch_NotExists, "The user does not exist.")); } } - [HttpGet("user/{username}/avatar"), Authorize] - public async Task GetAvatar([FromRoute] string username) - { - var url = await _userService.GetAvatarUrl(username); - if (url == null) - return NotFound(); - return Redirect(url); - } - - [HttpPut("user/{username}/avatar"), Authorize] - [Consumes("image/png", "image/gif", "image/jpeg", "image/svg+xml")] - public async Task PutAvatar([FromRoute] string username, [FromHeader(Name="Content-Type")] string contentType) + [HttpDelete("user/{username}"), AdminAuthorize] + public async Task Delete([FromRoute] string username) { - bool isAdmin = User.IsInRole("admin"); - if (!isAdmin) + try { - if (username != User.Identity.Name) - return StatusCode(StatusCodes.Status403Forbidden, PutAvatarResponse.Forbidden); + await _userService.DeleteUser(username); + _logger.LogInformation("A user is deleted. Username: {} .", username); + return Ok(CommonDeleteResponse.Deleted); } - - var stream = new MemoryStream(); - await Request.Body.CopyToAsync(stream); - var result = await _userService.PutAvatar(username, stream.ToArray(), contentType); - switch (result) + catch (UserNotExistException e) { - case PutAvatarResult.Success: - return Ok(PutAvatarResponse.Success); - case PutAvatarResult.UserNotExists: - return BadRequest(PutAvatarResponse.NotExists); - default: - throw new Exception("Unknown put avatar result."); + _logger.LogInformation(e, "Attempt to delete a non-existent user. Username: {} .", username); + return Ok(CommonDeleteResponse.NotExists); } } - [HttpPost("userop/changepassword"), Authorize] public async Task ChangePassword([FromBody] ChangePasswordRequest request) { - var result = await _userService.ChangePassword(User.Identity.Name, request.OldPassword, request.NewPassword); - switch (result) + try { - case ChangePasswordResult.Success: - return Ok(ChangePasswordResponse.Success); - case ChangePasswordResult.BadOldPassword: - return Ok(ChangePasswordResponse.BadOldPassword); - case ChangePasswordResult.NotExists: - return Ok(ChangePasswordResponse.NotExists); - default: - throw new Exception("Uncreachable code."); + await _userService.ChangePassword(User.Identity.Name, request.OldPassword, request.NewPassword); + _logger.LogInformation("A user changed password. Username: {} .", User.Identity.Name); + return Ok(); + } + catch (BadPasswordException e) + { + _logger.LogInformation(e, "A user attempt to change password but old password is wrong. Username: {} .", User.Identity.Name); + return BadRequest(new CommonResponse(ErrorCodes.ChangePassword_BadOldPassword, "Old password is wrong.")); } + // User can't be non-existent or the token is bad. } } } diff --git a/Timeline/Entities/Http/Common.cs b/Timeline/Entities/Http/Common.cs index 7708927a..3a45a0ae 100644 --- a/Timeline/Entities/Http/Common.cs +++ b/Timeline/Entities/Http/Common.cs @@ -1,13 +1,13 @@ namespace Timeline.Entities.Http { - public class CommonErrorResponse + public class CommonResponse { - public CommonErrorResponse() + public CommonResponse() { } - public CommonErrorResponse(int code, string message) + public CommonResponse(int code, string message) { Code = code; Message = message; @@ -16,4 +16,22 @@ public int Code { get; set; } public string Message { get; set; } } + + public static class CommonPutResponse + { + public const int CreatedCode = 0; + public const int ModifiedCode = 1; + + public static CommonResponse Created { get; } = new CommonResponse(CreatedCode, "A new item is created."); + public static CommonResponse Modified { get; } = new CommonResponse(ModifiedCode, "An existent item is modified."); + } + + public static class CommonDeleteResponse + { + public const int DeletedCode = 0; + public const int NotExistsCode = 1; + + public static CommonResponse Deleted { get; } = new CommonResponse(DeletedCode, "An existent item is deleted."); + public static CommonResponse NotExists { get; } = new CommonResponse(NotExistsCode, "The item does not exist."); + } } diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs index f5d233cd..91423c7b 100644 --- a/Timeline/Entities/Http/User.cs +++ b/Timeline/Entities/Http/User.cs @@ -12,49 +12,9 @@ public bool? IsAdmin { get; set; } } - public static class UserPutResponse - { - public const int CreatedCode = 0; - public const int ModifiedCode = 1; - - public static CommonErrorResponse Created { get; } = new CommonErrorResponse(CreatedCode, "A new user is created."); - public static CommonErrorResponse Modified { get; } = new CommonErrorResponse(ModifiedCode, "A existing user is modified."); - } - - public static class UserDeleteResponse - { - public const int DeletedCode = 0; - public const int NotExistsCode = 1; - - public static CommonErrorResponse Deleted { get; } = new CommonErrorResponse(DeletedCode, "A existing user is deleted."); - public static CommonErrorResponse NotExists { get; } = new CommonErrorResponse(NotExistsCode, "User with given name does not exists."); - } - public class ChangePasswordRequest { public string OldPassword { get; set; } public string NewPassword { get; set; } } - - public static class ChangePasswordResponse - { - public const int SuccessCode = 0; - public const int BadOldPasswordCode = 1; - public const int NotExistsCode = 2; - - public static CommonErrorResponse Success { get; } = new CommonErrorResponse(SuccessCode, "Success to change password."); - public static CommonErrorResponse BadOldPassword { get; } = new CommonErrorResponse(BadOldPasswordCode, "Old password is wrong."); - public static CommonErrorResponse NotExists { get; } = new CommonErrorResponse(NotExistsCode, "Username does not exists, please update token."); - } - - public static class PutAvatarResponse - { - public const int SuccessCode = 0; - public const int ForbiddenCode = 1; - public const int NotExistsCode = 2; - - public static CommonErrorResponse Success { get; } = new CommonErrorResponse(SuccessCode, "Success to upload avatar."); - public static CommonErrorResponse Forbidden { get; } = new CommonErrorResponse(ForbiddenCode, "You are not allowed to upload the user's avatar."); - public static CommonErrorResponse NotExists { get; } = new CommonErrorResponse(NotExistsCode, "The username does not exists. If you are a user, try update your token."); - } } diff --git a/Timeline/Entities/UserUtility.cs b/Timeline/Entities/UserUtility.cs index c8e82fba..cbbd391c 100644 --- a/Timeline/Entities/UserUtility.cs +++ b/Timeline/Entities/UserUtility.cs @@ -7,8 +7,8 @@ namespace Timeline.Entities { public static class UserUtility { - public const string UserRole = "user"; - public const string AdminRole = "admin"; + public const string UserRole = UserRoles.User; + public const string AdminRole = UserRoles.Admin; public static string[] UserRoleArray { get; } = new string[] { UserRole }; public static string[] AdminRoleArray { get; } = new string[] { UserRole, AdminRole }; diff --git a/Timeline/Models/DatabaseContext.cs b/Timeline/Models/DatabaseContext.cs index 87c0fd17..afd5a333 100644 --- a/Timeline/Models/DatabaseContext.cs +++ b/Timeline/Models/DatabaseContext.cs @@ -4,6 +4,12 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Timeline.Models { + public static class UserRoles + { + public const string Admin = "admin"; + public const string User = "user"; + } + [Table("user")] public class User { -- cgit v1.2.3 From cccaf4cfdc1b932882768975f409763c22ed1ee1 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 23 Jul 2019 19:03:25 +0800 Subject: Fix bugs and write unit tests. --- Timeline.Tests/AuthorizationUnitTest.cs | 18 +++++++++--------- .../Authentication/AuthenticationExtensions.cs | 9 +-------- Timeline.Tests/JwtTokenUnitTest.cs | 22 ++++++---------------- Timeline/Authenticate/AuthHandler.cs | 2 +- Timeline/Controllers/UserTestController.cs | 11 ++++++----- Timeline/Services/JwtService.cs | 2 +- Timeline/Services/PasswordService.cs | 2 ++ Timeline/Services/UserService.cs | 4 +--- 8 files changed, 27 insertions(+), 43 deletions(-) (limited to 'Timeline/Authenticate') diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs index 28715ada..ee3deac8 100644 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -10,9 +10,9 @@ namespace Timeline.Tests { public class AuthorizationUnitTest : IClassFixture> { - private const string NeedAuthorizeUrl = "Test/User/NeedAuthorize"; - private const string BothUserAndAdminUrl = "Test/User/BothUserAndAdmin"; - private const string OnlyAdminUrl = "Test/User/OnlyAdmin"; + private const string AuthorizeUrl = "Test/User/Authorize"; + private const string UserUrl = "Test/User/User"; + private const string AdminUrl = "Test/User/Admin"; private readonly WebApplicationFactory _factory; @@ -26,7 +26,7 @@ namespace Timeline.Tests { using (var client = _factory.CreateDefaultClient()) { - var response = await client.GetAsync(NeedAuthorizeUrl); + var response = await client.GetAsync(AuthorizeUrl); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } } @@ -36,7 +36,7 @@ namespace Timeline.Tests { using (var client = await _factory.CreateClientWithUser("user", "user")) { - var response = await client.GetAsync(NeedAuthorizeUrl); + var response = await client.GetAsync(AuthorizeUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } @@ -47,9 +47,9 @@ namespace Timeline.Tests using (var client = _factory.CreateDefaultClient()) { var token = (await client.CreateUserTokenAsync("user", "user")).Token; - var response1 = await client.SendWithAuthenticationAsync(token, BothUserAndAdminUrl); + var response1 = await client.SendWithAuthenticationAsync(token, UserUrl); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); - var response2 = await client.SendWithAuthenticationAsync(token, OnlyAdminUrl); + var response2 = await client.SendWithAuthenticationAsync(token, AdminUrl); Assert.Equal(HttpStatusCode.Forbidden, response2.StatusCode); } } @@ -59,9 +59,9 @@ namespace Timeline.Tests { using (var client = await _factory.CreateClientWithUser("admin", "admin")) { - var response1 = await client.GetAsync(BothUserAndAdminUrl); + var response1 = await client.GetAsync(UserUrl); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); - var response2 = await client.GetAsync(OnlyAdminUrl); + var response2 = await client.GetAsync(AdminUrl); Assert.Equal(HttpStatusCode.OK, response2.StatusCode); } } diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index cda9fe99..f4e2e45a 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; using System; -using System.Net; using System.Net.Http; using System.Threading.Tasks; using Timeline.Entities.Http; -using Xunit; namespace Timeline.Tests.Helpers.Authentication { @@ -13,15 +11,10 @@ namespace Timeline.Tests.Helpers.Authentication { private const string CreateTokenUrl = "/token/create"; - public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true) + public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - if (assertSuccess) - Assert.True(result.Success); - return result; } diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index a4e5432f..6ab4e8a6 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -28,11 +28,7 @@ namespace Timeline.Tests using (var client = _factory.CreateDefaultClient()) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "???", Password = "???" }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.False(result.Success); - Assert.Null(result.Token); - Assert.Null(result.UserInfo); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } @@ -44,9 +40,8 @@ namespace Timeline.Tests var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "user" }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.True(result.Success); Assert.NotNull(result.Token); - Assert.NotNull(result.UserInfo); + Assert.NotNull(result.User); } } @@ -56,11 +51,7 @@ namespace Timeline.Tests using (var client = _factory.CreateDefaultClient()) { var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var validationInfo = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.False(validationInfo.IsValid); - Assert.Null(validationInfo.UserInfo); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } @@ -75,10 +66,9 @@ namespace Timeline.Tests Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.True(result.IsValid); - Assert.NotNull(result.UserInfo); - Assert.Equal(createTokenResult.UserInfo.Username, result.UserInfo.Username); - Assert.Equal(createTokenResult.UserInfo.IsAdmin, result.UserInfo.IsAdmin); + Assert.NotNull(result.User); + Assert.Equal(createTokenResult.User.Username, result.User.Username); + Assert.Equal(createTokenResult.User.IsAdmin, result.User.IsAdmin); } } } diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs index 80bbaf14..80860edf 100644 --- a/Timeline/Authenticate/AuthHandler.cs +++ b/Timeline/Authenticate/AuthHandler.cs @@ -78,7 +78,7 @@ namespace Timeline.Authenticate { var userInfo = await _userService.VerifyToken(token); - var identity = new ClaimsIdentity(); + var identity = new ClaimsIdentity(AuthConstants.Scheme); identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); identity.AddClaims(Entities.UserUtility.IsAdminToRoleArray(userInfo.IsAdmin).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); diff --git a/Timeline/Controllers/UserTestController.cs b/Timeline/Controllers/UserTestController.cs index f1edb0d5..21686b81 100644 --- a/Timeline/Controllers/UserTestController.cs +++ b/Timeline/Controllers/UserTestController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Timeline.Authenticate; namespace Timeline.Controllers { @@ -8,21 +9,21 @@ namespace Timeline.Controllers { [HttpGet("[action]")] [Authorize] - public ActionResult NeedAuthorize() + public ActionResult Authorize() { return Ok(); } [HttpGet("[action]")] - [Authorize(Roles = "user,admin")] - public ActionResult BothUserAndAdmin() + [UserAuthorize] + public new ActionResult User() { return Ok(); } [HttpGet("[action]")] - [Authorize(Roles = "admin")] - public ActionResult OnlyAdmin() + [AdminAuthorize] + public ActionResult Admin() { return Ok(); } diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index f721971b..e970bbd4 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -126,7 +126,7 @@ namespace Timeline.Services Version = version }; } - catch (SecurityTokenException e) + catch (Exception e) { throw new JwtTokenVerifyException("Validate token failed caused by a SecurityTokenException. See inner exception.", e); } diff --git a/Timeline/Services/PasswordService.cs b/Timeline/Services/PasswordService.cs index 8eab526e..106080f1 100644 --- a/Timeline/Services/PasswordService.cs +++ b/Timeline/Services/PasswordService.cs @@ -24,6 +24,8 @@ namespace Timeline.Services bool VerifyPassword(string hashedPassword, string providedPassword); } + //TODO! Use exceptions!!! + /// /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs /// Remove V2 format and unnecessary format version check. diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index ec8e5091..01d05903 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -153,16 +153,14 @@ namespace Timeline.Services private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; - private readonly IQCloudCosService _cosService; - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService) + public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) { _logger = logger; _memoryCache = memoryCache; _databaseContext = databaseContext; _jwtService = jwtService; _passwordService = passwordService; - _cosService = cosService; } private string GenerateCacheKeyByUserId(long id) => $"user:{id}"; -- cgit v1.2.3