aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-07-27 21:47:14 +0800
committerGitHub <noreply@github.com>2019-07-27 21:47:14 +0800
commit590a8c576f17817539505ef2ca50f52e840a61d2 (patch)
tree572a2ae5c65c484718b3bfda68fd8babc56fe6f2
parent3de4179449a209646e0e5a967d270f7fa0878c03 (diff)
parent58985e8f2a6931029974067b2c1e78963e4508f0 (diff)
downloadtimeline-590a8c576f17817539505ef2ca50f52e840a61d2.tar.gz
timeline-590a8c576f17817539505ef2ca50f52e840a61d2.tar.bz2
timeline-590a8c576f17817539505ef2ca50f52e840a61d2.zip
Merge pull request #25 from crupest/auth
Refactor a lot, especially authentication.
-rw-r--r--Timeline.Tests/AuthorizationUnitTest.cs18
-rw-r--r--Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs9
-rw-r--r--Timeline.Tests/JwtTokenUnitTest.cs22
-rw-r--r--Timeline.Tests/QCloudCosServiceUnitTest.cs68
-rw-r--r--Timeline/Authenticate/Attribute.cs21
-rw-r--r--Timeline/Authenticate/AuthHandler.cs97
-rw-r--r--Timeline/Configs/JwtConfig.cs6
-rw-r--r--Timeline/Controllers/TokenController.cs80
-rw-r--r--Timeline/Controllers/UserController.cs130
-rw-r--r--Timeline/Controllers/UserTestController.cs11
-rw-r--r--Timeline/Entities/Http/Common.cs38
-rw-r--r--Timeline/Entities/Http/Token.cs6
-rw-r--r--Timeline/Entities/Http/User.cs40
-rw-r--r--Timeline/Entities/PutResult.cs17
-rw-r--r--Timeline/Entities/UserInfo.cs4
-rw-r--r--Timeline/Entities/UserUtility.cs19
-rw-r--r--Timeline/Formatters/StringInputFormatter.cs29
-rw-r--r--Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs49
-rw-r--r--Timeline/Migrations/20190719115321_Add-User-Version.cs23
-rw-r--r--Timeline/Migrations/DatabaseContextModelSnapshot.cs5
-rw-r--r--Timeline/Models/DatabaseContext.cs9
-rw-r--r--Timeline/Services/JwtService.cs79
-rw-r--r--Timeline/Services/PasswordService.cs2
-rw-r--r--Timeline/Services/QCloudCosService.cs1
-rw-r--r--Timeline/Services/UserService.cs352
-rw-r--r--Timeline/Startup.cs39
26 files changed, 667 insertions, 507 deletions
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<WebApplicationFactory<Startup>>
{
- 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<Startup> _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<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true)
+ public static async Task<CreateTokenResponse> 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<CreateTokenResponse>(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<CreateTokenResponse>(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<CreateTokenResponse>(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<VerifyTokenResponse>(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<VerifyTokenResponse>(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.Tests/QCloudCosServiceUnitTest.cs b/Timeline.Tests/QCloudCosServiceUnitTest.cs
index 0940c70d..c330cce0 100644
--- a/Timeline.Tests/QCloudCosServiceUnitTest.cs
+++ b/Timeline.Tests/QCloudCosServiceUnitTest.cs
@@ -1,10 +1,6 @@
using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Threading.Tasks;
using Timeline.Services;
using Timeline.Tests.Helpers;
using Xunit;
@@ -65,40 +61,40 @@ namespace Timeline.Tests
Assert.Equal("q-sign-algorithm=sha1&q-ak=AKIDQjz3ltompVjBni5LitkWHFlFpwkn9U5q&q-sign-time=1417773892;1417853898&q-key-time=1417773892;1417853898&q-header-list=host;x-cos-content-sha1;x-cos-storage-class&q-url-param-list=&q-signature=0ab12f43e74cbe148d705cd9fae8adc9a6d39cc1", QCloudCosService.GenerateSign(credential, request, signValidTime));
}
-/*
-// Tests in this part need secret configs in cos.
-#region SecretTests
- [Fact]
- public async Task ObjectExistsTest()
- {
- using (var serviceScope = _factory.Server.Host.Services.CreateScope())
- {
- var services = serviceScope.ServiceProvider;
- var service = services.GetRequiredService<IQCloudCosService>();
- Assert.True(await service.IsObjectExists("avatar", "__default"));
- Assert.False(await service.IsObjectExists("avatar", "haha"));
- Assert.False(await service.IsObjectExists("haha", "haha"));
- }
- }
+ /*
+ // Tests in this part need secret configs in cos.
+ #region SecretTests
+ [Fact]
+ public async Task ObjectExistsTest()
+ {
+ using (var serviceScope = _factory.Server.Host.Services.CreateScope())
+ {
+ var services = serviceScope.ServiceProvider;
+ var service = services.GetRequiredService<IQCloudCosService>();
+ Assert.True(await service.IsObjectExists("avatar", "__default"));
+ Assert.False(await service.IsObjectExists("avatar", "haha"));
+ Assert.False(await service.IsObjectExists("haha", "haha"));
+ }
+ }
- [Fact]
- public async Task GenerateObjectGetUrlTest()
- {
- using (var serviceScope = _factory.Server.Host.Services.CreateScope())
- {
- var services = serviceScope.ServiceProvider;
- var service = services.GetRequiredService<IQCloudCosService>();
- var url = service.GenerateObjectGetUrl("avatar", "__default");
- // never use the following line! Because client created by factory can't access Internet.
- //using (var client = _factory.CreateClient())
- using (var client = services.GetRequiredService<IHttpClientFactory>().CreateClient())
+ [Fact]
+ public async Task GenerateObjectGetUrlTest()
{
- var res = await client.GetAsync(url);
- Assert.Equal(HttpStatusCode.OK, res.StatusCode);
+ using (var serviceScope = _factory.Server.Host.Services.CreateScope())
+ {
+ var services = serviceScope.ServiceProvider;
+ var service = services.GetRequiredService<IQCloudCosService>();
+ var url = service.GenerateObjectGetUrl("avatar", "__default");
+ // never use the following line! Because client created by factory can't access Internet.
+ //using (var client = _factory.CreateClient())
+ using (var client = services.GetRequiredService<IHttpClientFactory>().CreateClient())
+ {
+ var res = await client.GetAsync(url);
+ Assert.Equal(HttpStatusCode.OK, res.StatusCode);
+ }
+ }
}
- }
- }
-#endregion
-*/
+ #endregion
+ */
}
}
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/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs
new file mode 100644
index 00000000..80860edf
--- /dev/null
+++ b/Timeline/Authenticate/AuthHandler.cs
@@ -0,0 +1,97 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Timeline.Services;
+
+namespace Timeline.Authenticate
+{
+ static class AuthConstants
+ {
+ public const string Scheme = "Bearer";
+ public const string DisplayName = "My Jwt Auth Scheme";
+ }
+
+ class AuthOptions : AuthenticationSchemeOptions
+ {
+ /// <summary>
+ /// The query param key to search for token. If null then query params are not searched for token. Default to <c>"token"</c>.
+ /// </summary>
+ public string TokenQueryParamKey { get; set; } = "token";
+ }
+
+ class AuthHandler : AuthenticationHandler<AuthOptions>
+ {
+ private readonly ILogger<AuthHandler> _logger;
+ private readonly IUserService _userService;
+
+ public AuthHandler(IOptionsMonitor<AuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService)
+ : base(options, logger, encoder, clock)
+ {
+ _logger = logger.CreateLogger<AuthHandler>();
+ _userService = userService;
+ }
+
+ // 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<AuthenticateResult> HandleAuthenticateAsync()
+ {
+ var token = ExtractToken();
+ if (string.IsNullOrEmpty(token))
+ {
+ _logger.LogInformation("No jwt token is found.");
+ return AuthenticateResult.NoResult();
+ }
+
+ try
+ {
+ var userInfo = await _userService.VerifyToken(token);
+
+ 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)));
+
+ var principal = new ClaimsPrincipal();
+ principal.AddIdentity(identity);
+
+ return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme));
+ }
+ catch (Exception e)
+ {
+ _logger.LogInformation(e, "A jwt token validation failed.");
+ return AuthenticateResult.Fail(e);
+ }
+ }
+ }
+}
diff --git a/Timeline/Configs/JwtConfig.cs b/Timeline/Configs/JwtConfig.cs
index 9550424e..4d5ef97f 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; }
+
+ /// <summary>
+ /// Set the default value of expire offset of jwt token.
+ /// Unit is second. Default is 3600 * 24 seconds, aka 1 day.
+ /// </summary>
+ public long DefaultExpireOffset { get; set; } = 3600 * 24;
}
}
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs
index 0be5fb2f..023bd53f 100644
--- a/Timeline/Controllers/TokenController.cs
+++ b/Timeline/Controllers/TokenController.cs
@@ -12,8 +12,21 @@ namespace Timeline.Controllers
{
private static class LoggingEventIds
{
- public const int LogInSucceeded = 4000;
- public const int LogInFailed = 4001;
+ public const int LogInSucceeded = 1000;
+ public const int LogInFailed = 1001;
+
+ public const int VerifySucceeded = 2000;
+ public const int VerifyFailed = 2001;
+ }
+
+ private static class ErrorCodes
+ {
+ 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;
}
private readonly IUserService _userService;
@@ -27,48 +40,63 @@ namespace Timeline.Controllers
[HttpPost("create")]
[AllowAnonymous]
- public async Task<ActionResult<CreateTokenResponse>> Create([FromBody] CreateTokenRequest request)
+ public async Task<IActionResult> Create([FromBody] CreateTokenRequest request)
{
- var result = await _userService.CreateToken(request.Username, request.Password);
-
- if (result == null)
+ try
{
- _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login with username: {} and password: {} failed.", request.Username, request.Password);
+ var result = await _userService.CreateToken(request.Username, request.Password);
+ _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login succeeded. Username: {} .", request.Username);
return Ok(new CreateTokenResponse
{
- Success = false
+ Token = result.Token,
+ User = result.User
});
}
-
- _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", request.Username);
-
- return Ok(new CreateTokenResponse
+ catch(UserNotExistException e)
+ {
+ var code = ErrorCodes.Create_UserNotExist;
+ _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)
{
- Success = true,
- Token = result.Token,
- UserInfo = result.UserInfo
- });
+ var code = ErrorCodes.Create_BadPassword;
+ _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."));
+ }
}
[HttpPost("verify")]
[AllowAnonymous]
- public async Task<ActionResult<VerifyTokenResponse>> Verify([FromBody] VerifyTokenRequest request)
+ public async Task<IActionResult> Verify([FromBody] VerifyTokenRequest request)
{
- var result = await _userService.VerifyToken(request.Token);
-
- if (result == null)
+ try
{
+ var result = await _userService.VerifyToken(request.Token);
+ _logger.LogInformation(LoggingEventIds.VerifySucceeded, "Verify token succeeded. Username: {} Token: {} .", result.Username, request.Token);
return Ok(new VerifyTokenResponse
{
- IsValid = false,
+ User = result
});
}
-
- return Ok(new VerifyTokenResponse
+ catch (JwtTokenVerifyException e)
{
- IsValid = true,
- UserInfo = result
- });
+ var code = ErrorCodes.Verify_BadToken;
+ _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 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 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<UserController> _logger;
private readonly IUserService _userService;
- public UserController(IUserService userService)
+ public UserController(ILogger<UserController> logger, IUserService userService)
{
+ _logger = logger;
_userService = userService;
}
- [HttpGet("users"), Authorize(Roles = "admin")]
+ [HttpGet("users"), AdminAuthorize]
public async Task<ActionResult<UserInfo[]>> List()
{
return Ok(await _userService.ListUsers());
}
- [HttpGet("user/{username}"), Authorize]
+ [HttpGet("user/{username}"), AdminAuthorize]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> PutAvatar([FromRoute] string username, [FromHeader(Name="Content-Type")] string contentType)
+ [HttpDelete("user/{username}"), AdminAuthorize]
+ public async Task<IActionResult> 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<IActionResult> 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/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/Entities/Http/Common.cs b/Timeline/Entities/Http/Common.cs
index 9575e6fa..3a45a0ae 100644
--- a/Timeline/Entities/Http/Common.cs
+++ b/Timeline/Entities/Http/Common.cs
@@ -1,29 +1,37 @@
namespace Timeline.Entities.Http
{
- public class ReturnCodeMessageResponse
+ public class CommonResponse
{
- public ReturnCodeMessageResponse()
+ public CommonResponse()
{
}
- public ReturnCodeMessageResponse(int code)
- {
- ReturnCode = code;
- }
-
- public ReturnCodeMessageResponse(string message)
+ public CommonResponse(int code, string message)
{
+ Code = code;
Message = message;
}
- public ReturnCodeMessageResponse(int code, string message)
- {
- ReturnCode = code;
- Message = message;
- }
+ 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 int? ReturnCode { get; set; } = null;
- public string Message { get; set; } = null;
+ 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/Token.cs b/Timeline/Entities/Http/Token.cs
index 45ee0fc5..aeb9fbf2 100644
--- a/Timeline/Entities/Http/Token.cs
+++ b/Timeline/Entities/Http/Token.cs
@@ -8,9 +8,8 @@
public class CreateTokenResponse
{
- public bool Success { get; set; }
public string Token { get; set; }
- public UserInfo UserInfo { get; set; }
+ public UserInfo User { get; set; }
}
public class VerifyTokenRequest
@@ -20,7 +19,6 @@
public class VerifyTokenResponse
{
- public bool IsValid { get; set; }
- public UserInfo UserInfo { get; set; }
+ public UserInfo User { get; set; }
}
}
diff --git a/Timeline/Entities/Http/User.cs b/Timeline/Entities/Http/User.cs
index db3d5071..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 ReturnCodeMessageResponse Created { get; } = new ReturnCodeMessageResponse(CreatedCode, "A new user is created.");
- public static ReturnCodeMessageResponse Modified { get; } = new ReturnCodeMessageResponse(ModifiedCode, "A existing user is modified.");
- }
-
- public static class UserDeleteResponse
- {
- public const int DeletedCode = 0;
- public const int NotExistsCode = 1;
-
- public static ReturnCodeMessageResponse Deleted { get; } = new ReturnCodeMessageResponse(DeletedCode, "A existing user is deleted.");
- public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(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 ReturnCodeMessageResponse Success { get; } = new ReturnCodeMessageResponse(SuccessCode, "Success to change password.");
- public static ReturnCodeMessageResponse BadOldPassword { get; } = new ReturnCodeMessageResponse(BadOldPasswordCode, "Old password is wrong.");
- public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(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 ReturnCodeMessageResponse Success { get; } = new ReturnCodeMessageResponse(SuccessCode, "Success to upload avatar.");
- public static ReturnCodeMessageResponse Forbidden { get; } = new ReturnCodeMessageResponse(ForbiddenCode, "You are not allowed to upload the user's avatar.");
- public static ReturnCodeMessageResponse NotExists { get; } = new ReturnCodeMessageResponse(NotExistsCode, "The username does not exists. If you are a user, try update your token.");
- }
}
diff --git a/Timeline/Entities/PutResult.cs b/Timeline/Entities/PutResult.cs
new file mode 100644
index 00000000..4ed48572
--- /dev/null
+++ b/Timeline/Entities/PutResult.cs
@@ -0,0 +1,17 @@
+namespace Timeline.Entities
+{
+ /// <summary>
+ /// Represents the result of a "put" operation.
+ /// </summary>
+ public enum PutResult
+ {
+ /// <summary>
+ /// Indicates the item did not exist and now is created.
+ /// </summary>
+ Created,
+ /// <summary>
+ /// Indicates the item exists already and is modified.
+ /// </summary>
+ Modified
+ }
+}
diff --git a/Timeline/Entities/UserInfo.cs b/Timeline/Entities/UserInfo.cs
index bb56df9d..9a82c991 100644
--- a/Timeline/Entities/UserInfo.cs
+++ b/Timeline/Entities/UserInfo.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
namespace Timeline.Entities
{
public sealed class UserInfo
diff --git a/Timeline/Entities/UserUtility.cs b/Timeline/Entities/UserUtility.cs
index 9a272948..cbbd391c 100644
--- a/Timeline/Entities/UserUtility.cs
+++ b/Timeline/Entities/UserUtility.cs
@@ -1,14 +1,14 @@
using System;
using System.Linq;
-using Timeline.Entities;
using Timeline.Models;
+using Timeline.Services;
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 };
@@ -38,12 +38,23 @@ namespace Timeline.Entities
return RoleArrayToRoleString(IsAdminToRoleArray(isAdmin));
}
+ public static bool RoleStringToIsAdmin(string roleString)
+ {
+ return RoleArrayToIsAdmin(RoleStringToRoleArray(roleString));
+ }
+
public static UserInfo CreateUserInfo(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
- return new UserInfo(user.Name, RoleArrayToIsAdmin(RoleStringToRoleArray(user.RoleString)));
+ return new UserInfo(user.Name, RoleStringToIsAdmin(user.RoleString));
}
+ internal static UserCache CreateUserCache(User user)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+ return new UserCache { Username = user.Name, IsAdmin = RoleStringToIsAdmin(user.RoleString), Version = user.Version };
+ }
}
}
diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs
deleted file mode 100644
index ca9216d7..00000000
--- a/Timeline/Formatters/StringInputFormatter.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Microsoft.AspNetCore.Mvc.Formatters;
-using Microsoft.Net.Http.Headers;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Timeline.Formatters
-{
- public class StringInputFormatter : TextInputFormatter
- {
- public StringInputFormatter()
- {
- SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain"));
-
- SupportedEncodings.Add(Encoding.UTF8);
- SupportedEncodings.Add(Encoding.Unicode);
- }
-
- public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
- {
- var request = context.HttpContext.Request;
- using (var reader = new StreamReader(request.Body, effectiveEncoding))
- {
- var stringContent = reader.ReadToEnd();
- return InputFormatterResult.SuccessAsync(stringContent);
- }
- }
- }
-}
diff --git a/Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs b/Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs
new file mode 100644
index 00000000..42eeeb40
--- /dev/null
+++ b/Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs
@@ -0,0 +1,49 @@
+// <auto-generated />
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Timeline.Models;
+
+namespace Timeline.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20190719115321_Add-User-Version")]
+ partial class AddUserVersion
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Timeline.Models.User", b =>
+ {
+ b.Property<long>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property<string>("EncryptedPassword")
+ .IsRequired()
+ .HasColumnName("password");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnName("name");
+
+ b.Property<string>("RoleString")
+ .IsRequired()
+ .HasColumnName("roles");
+
+ b.Property<long>("Version")
+ .HasColumnName("version");
+
+ b.HasKey("Id");
+
+ b.ToTable("user");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Timeline/Migrations/20190719115321_Add-User-Version.cs b/Timeline/Migrations/20190719115321_Add-User-Version.cs
new file mode 100644
index 00000000..715af909
--- /dev/null
+++ b/Timeline/Migrations/20190719115321_Add-User-Version.cs
@@ -0,0 +1,23 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Timeline.Migrations
+{
+ public partial class AddUserVersion : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<long>(
+ name: "version",
+ table: "user",
+ nullable: false,
+ defaultValue: 0L);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "version",
+ table: "user");
+ }
+ }
+}
diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs
index a833d2dc..7d244969 100644
--- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs
+++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs
@@ -13,7 +13,7 @@ namespace Timeline.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "2.2.3-servicing-35854")
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Timeline.Models.User", b =>
@@ -34,6 +34,9 @@ namespace Timeline.Migrations
.IsRequired()
.HasColumnName("roles");
+ b.Property<long>("Version")
+ .HasColumnName("version");
+
b.HasKey("Id");
b.ToTable("user");
diff --git a/Timeline/Models/DatabaseContext.cs b/Timeline/Models/DatabaseContext.cs
index 1e89ea82..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
{
@@ -18,6 +24,9 @@ namespace Timeline.Models
[Column("roles"), Required]
public string RoleString { get; set; }
+
+ [Column("version"), Required]
+ public long Version { get; set; }
}
public class DatabaseContext : DbContext
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs
index f5df59a5..e970bbd4 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/JwtService.cs
@@ -1,77 +1,85 @@
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
+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;
-using Timeline.Entities;
namespace Timeline.Services
{
public class TokenInfo
{
- public string Name { get; set; }
- public string[] Roles { get; set; }
+ public long Id { 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
{
/// <summary>
- /// Create a JWT token for a given user info.
+ /// Create a JWT token for a given token info.
/// </summary>
/// <param name="tokenInfo">The info to generate token.</param>
+ /// <param name="expires">The expire time. If null then use current time with offset in config.</param>
/// <returns>Return the generated token.</returns>
- string GenerateJwtToken(TokenInfo tokenInfo);
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
+ string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null);
/// <summary>
/// Verify a JWT token.
/// Return null is <paramref name="token"/> is null.
/// </summary>
/// <param name="token">The token string to verify.</param>
- /// <returns>Return null if <paramref name="token"/> is null or token is invalid. Return the saved info otherwise.</returns>
+ /// <returns>Return the saved info in token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="JwtTokenVerifyException">Thrown when the token is invalid.</exception>
TokenInfo VerifyJwtToken(string token);
}
public class JwtService : IJwtService
{
+ private const string VersionClaimType = "timeline_version";
+
private readonly IOptionsMonitor<JwtConfig> _jwtConfig;
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
- private readonly ILogger<JwtService> _logger;
- public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, ILogger<JwtService> logger)
+ public JwtService(IOptionsMonitor<JwtConfig> jwtConfig)
{
_jwtConfig = jwtConfig;
- _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));
- 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(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64));
+ 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);
@@ -84,7 +92,7 @@ namespace Timeline.Services
public TokenInfo VerifyJwtToken(string token)
{
if (token == null)
- return null;
+ throw new ArgumentNullException(nameof(token));
var config = _jwtConfig.CurrentValue;
try
@@ -98,18 +106,29 @@ namespace Timeline.Services
ValidIssuer = config.Issuer,
ValidAudience = config.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
- }, out SecurityToken validatedToken);
+ }, out _);
+
+ var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
+ if (idClaim == null)
+ throw new JwtTokenVerifyException("Id claim does not exist.");
+ if (!long.TryParse(idClaim, out var id))
+ throw new JwtTokenVerifyException("Can't convert id claim into a integer number.");
+
+ 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()
+ Id = id,
+ Version = version
};
}
catch (Exception 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);
}
}
}
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!!!
+
/// <summary>
/// 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/QCloudCosService.cs b/Timeline/Services/QCloudCosService.cs
index b37631e5..748173c4 100644
--- a/Timeline/Services/QCloudCosService.cs
+++ b/Timeline/Services/QCloudCosService.cs
@@ -6,7 +6,6 @@ using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 9fe9e08f..01d05903 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
@@ -12,71 +13,41 @@ namespace Timeline.Services
public class CreateTokenResult
{
public string Token { get; set; }
- public UserInfo UserInfo { get; set; }
+ public UserInfo User { get; set; }
}
- public enum PutUserResult
+ [Serializable]
+ public class UserNotExistException : Exception
{
- /// <summary>
- /// A new user is created.
- /// </summary>
- Created,
- /// <summary>
- /// A existing user is modified.
- /// </summary>
- Modified
+ public UserNotExistException(): base("The user does not exist.") { }
+ public UserNotExistException(string message) : base(message) { }
+ public UserNotExistException(string message, Exception inner) : base(message, inner) { }
+ protected UserNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
- public enum PatchUserResult
+ [Serializable]
+ public class BadPasswordException : Exception
{
- /// <summary>
- /// Succeed to modify user.
- /// </summary>
- Success,
- /// <summary>
- /// A user of given username does not exist.
- /// </summary>
- NotExists
+ public BadPasswordException(): base("Password is wrong.") { }
+ public BadPasswordException(string message) : base(message) { }
+ public BadPasswordException(string message, Exception inner) : base(message, inner) { }
+ protected BadPasswordException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
- public enum DeleteUserResult
- {
- /// <summary>
- /// A existing user is deleted.
- /// </summary>
- Deleted,
- /// <summary>
- /// A user of given username does not exist.
- /// </summary>
- NotExists
- }
- public enum ChangePasswordResult
+ [Serializable]
+ public class BadTokenVersionException : Exception
{
- /// <summary>
- /// Success to change password.
- /// </summary>
- Success,
- /// <summary>
- /// The user does not exists.
- /// </summary>
- NotExists,
- /// <summary>
- /// Old password is wrong.
- /// </summary>
- BadOldPassword
- }
-
- public enum PutAvatarResult
- {
- /// <summary>
- /// Success to upload avatar.
- /// </summary>
- Success,
- /// <summary>
- /// The user does not exists.
- /// </summary>
- UserNotExists
+ public BadTokenVersionException(): base("Token version is expired.") { }
+ public BadTokenVersionException(string message) : base(message) { }
+ public BadTokenVersionException(string message, Exception inner) : base(message, inner) { }
+ protected BadTokenVersionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
public interface IUserService
@@ -85,9 +56,12 @@ namespace Timeline.Services
/// Try to anthenticate with the given username and password.
/// If success, create a token and return the user info.
/// </summary>
- /// <param name="username">The username of the user to be anthenticated.</param>
- /// <param name="password">The password of the user to be anthenticated.</param>
- /// <returns>Return null if anthentication failed. An <see cref="CreateTokenResult"/> containing the created token and user info if anthentication succeeded.</returns>
+ /// <param name="username">The username of the user to anthenticate.</param>
+ /// <param name="password">The password of the user to anthenticate.</param>
+ /// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
Task<CreateTokenResult> CreateToken(string username, string password);
/// <summary>
@@ -95,7 +69,11 @@ namespace Timeline.Services
/// If success, return the user info.
/// </summary>
/// <param name="token">The token to verify.</param>
- /// <returns>Return null if verification failed. The user info if verification succeeded.</returns>
+ /// <returns>The user info specified by the token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="JwtTokenVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
+ /// <exception cref="BadTokenVersionException">Thrown when the version in the token is expired. User needs to recreate the token.</exception>
Task<UserInfo> VerifyToken(string token);
/// <summary>
@@ -118,31 +96,29 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">Username of user.</param>
/// <param name="password">Password of user.</param>
- /// <param name="roles">Array of roles of user.</param>
- /// <returns>Return <see cref="PutUserResult.Created"/> if a new user is created.
- /// Return <see cref="PutUserResult.Modified"/> if a existing user is modified.</returns>
- Task<PutUserResult> PutUser(string username, string password, bool isAdmin);
+ /// <param name="isAdmin">Whether the user is administrator.</param>
+ /// <returns>Return <see cref="PutResult.Created"/> if a new user is created.
+ /// Return <see cref="PutResult.Modified"/> if a existing user is modified.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ Task<PutResult> PutUser(string username, string password, bool isAdmin);
/// <summary>
- /// Partially modify a use of given username.
+ /// Partially modify a user of given username.
/// </summary>
- /// <param name="username">Username of the user to modify.</param>
- /// <param name="password">New password. If not modify, then null.</param>
- /// <param name="roles">New roles. If not modify, then null.</param>
- /// <returns>Return <see cref="PatchUserResult.Success"/> if modification succeeds.
- /// Return <see cref="PatchUserResult.NotExists"/> if the user of given username doesn't exist.</returns>
- Task<PatchUserResult> PatchUser(string username, string password, bool? isAdmin);
+ /// <param name="username">Username of the user to modify. Can't be null.</param>
+ /// <param name="password">New password. Null if not modify.</param>
+ /// <param name="isAdmin">Whether the user is administrator. Null if not modify.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ Task PatchUser(string username, string password, bool? isAdmin);
/// <summary>
/// Delete a user of given username.
- /// Return <see cref="DeleteUserResult.Deleted"/> if the user is deleted.
- /// Return <see cref="DeleteUserResult.NotExists"/> if the user of given username
- /// does not exist.
/// </summary>
- /// <param name="username">Username of thet user to delete.</param>
- /// <returns><see cref="DeleteUserResult.Deleted"/> if the user is deleted.
- /// <see cref="DeleteUserResult.NotExists"/> if the user doesn't exist.</returns>
- Task<DeleteUserResult> DeleteUser(string username);
+ /// <param name="username">Username of thet user to delete. Can't be null.</param>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ Task DeleteUser(string username);
/// <summary>
/// Try to change a user's password with old password.
@@ -150,90 +126,126 @@ namespace Timeline.Services
/// <param name="username">The name of user to change password of.</param>
/// <param name="oldPassword">The user's old password.</param>
/// <param name="newPassword">The user's new password.</param>
- /// <returns><see cref="ChangePasswordResult.Success"/> if success.
- /// <see cref="ChangePasswordResult.NotExists"/> if user does not exist.
- /// <see cref="ChangePasswordResult.BadOldPassword"/> if old password is wrong.</returns>
- Task<ChangePasswordResult> ChangePassword(string username, string oldPassword, string newPassword);
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> or <paramref name="oldPassword"/> or <paramref name="newPassword"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown if the old password is wrong.</exception>
+ Task ChangePassword(string username, string oldPassword, string newPassword);
+ }
- /// <summary>
- /// Get the true avatar url of a user.
- /// </summary>
- /// <param name="username">The name of user.</param>
- /// <returns>The url if user exists. Null if user does not exist.</returns>
- Task<string> GetAvatarUrl(string username);
+ internal class UserCache
+ {
+ public string Username { get; set; }
+ public bool IsAdmin { get; set; }
+ public long Version { get; set; }
- /// <summary>
- /// Put a avatar of a user.
- /// </summary>
- /// <param name="username">The name of user.</param>
- /// <param name="data">The data of avatar image.</param>
- /// <param name="mimeType">The mime type of the image.</param>
- /// <returns>Return <see cref="PutAvatarResult.Success"/> if success.
- /// Return <see cref="PutAvatarResult.UserNotExists"/> if user does not exist.</returns>
- Task<PutAvatarResult> PutAvatar(string username, byte[] data, string mimeType);
+ public UserInfo ToUserInfo()
+ {
+ return new UserInfo(Username, IsAdmin);
+ }
}
public class UserService : IUserService
{
private readonly ILogger<UserService> _logger;
+
+ private readonly IMemoryCache _memoryCache;
private readonly DatabaseContext _databaseContext;
+
private readonly IJwtService _jwtService;
private readonly IPasswordService _passwordService;
- private readonly IQCloudCosService _cosService;
- public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService)
+ public UserService(ILogger<UserService> 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}";
+
+ private void RemoveCache(long id)
+ {
+ _memoryCache.Remove(GenerateCacheKeyByUserId(id));
}
public async Task<CreateTokenResult> CreateToken(string username, string password)
{
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ // We need password info, so always check the database.
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
{
- _logger.LogInformation($"Create token failed with invalid username. Username = {username} Password = {password} .");
- return null;
+ var e = new UserNotExistException();
+ _logger.LogInformation(e, $"Create token failed. Reason: invalid username. Username = {username} Password = {password} .");
+ throw e;
}
- var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, password);
-
- if (verifyResult)
+ if (!_passwordService.VerifyPassword(user.EncryptedPassword, password))
{
- var roles = RoleStringToRoleArray(user.RoleString);
- var token = _jwtService.GenerateJwtToken(new TokenInfo
- {
- Name = username,
- Roles = roles
- });
- return new CreateTokenResult
- {
- Token = token,
- UserInfo = new UserInfo(username, RoleArrayToIsAdmin(roles))
- };
+ var e = new BadPasswordException();
+ _logger.LogInformation(e, $"Create token failed. Reason: invalid password. Username = {username} Password = {password} .");
+ throw e;
}
- else
+
+ var token = _jwtService.GenerateJwtToken(new TokenInfo
{
- _logger.LogInformation($"Create token failed with invalid password. Username = {username} Password = {password} .");
- return null;
- }
+ Id = user.Id,
+ Version = user.Version
+ });
+ return new CreateTokenResult
+ {
+ Token = token,
+ User = CreateUserInfo(user)
+ };
}
public async Task<UserInfo> VerifyToken(string token)
{
- var tokenInfo = _jwtService.VerifyJwtToken(token);
+ TokenInfo tokenInfo;
+ try
+ {
+ tokenInfo = _jwtService.VerifyJwtToken(token);
+ }
+ catch (JwtTokenVerifyException e)
+ {
+ _logger.LogInformation(e, $"Verify token falied. Reason: invalid token. Token: {token} .");
+ throw e;
+ }
- if (tokenInfo == null)
+ var id = tokenInfo.Id;
+ var key = GenerateCacheKeyByUserId(id);
+ if (!_memoryCache.TryGetValue<UserCache>(key, out var cache))
{
- _logger.LogInformation($"Verify token falied. Reason: invalid token. Token: {token} .");
- return null;
+ // no cache, check the database
+ var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ var e = new UserNotExistException();
+ _logger.LogInformation(e, $"Verify token falied. Reason: invalid id. Token: {token} Id: {id}.");
+ throw e;
+ }
+
+ // create cache
+ cache = CreateUserCache(user);
+ _memoryCache.CreateEntry(key).SetValue(cache);
}
- return await Task.FromResult(new UserInfo(tokenInfo.Name, RoleArrayToIsAdmin(tokenInfo.Roles)));
+ if (tokenInfo.Version != cache.Version)
+ {
+ var e = new BadTokenVersionException();
+ _logger.LogInformation(e, $"Verify token falied. Reason: invalid version. Token: {token} Id: {id} Username: {cache.Username} Version: {tokenInfo.Version} Version in cache: {cache.Version}.");
+ throw e;
+ }
+
+ return cache.ToUserInfo();
}
public async Task<UserInfo> GetUser(string username)
@@ -251,8 +263,13 @@ namespace Timeline.Services
.ToArrayAsync();
}
- public async Task<PutUserResult> PutUser(string username, string password, bool isAdmin)
+ public async Task<PutResult> PutUser(string username, string password, bool isAdmin)
{
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
@@ -261,25 +278,32 @@ namespace Timeline.Services
{
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = IsAdminToRoleString(isAdmin)
+ RoleString = IsAdminToRoleString(isAdmin),
+ Version = 0
});
await _databaseContext.SaveChangesAsync();
- return PutUserResult.Created;
+ return PutResult.Created;
}
user.EncryptedPassword = _passwordService.HashPassword(password);
user.RoleString = IsAdminToRoleString(isAdmin);
+ user.Version += 1;
await _databaseContext.SaveChangesAsync();
- return PutUserResult.Modified;
+ //clear cache
+ RemoveCache(user.Id);
+
+ return PutResult.Modified;
}
- public async Task<PatchUserResult> PatchUser(string username, string password, bool? isAdmin)
+ public async Task PatchUser(string username, string password, bool? isAdmin)
{
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
- return PatchUserResult.NotExists;
+ throw new UserNotExistException();
bool modified = false;
@@ -297,70 +321,50 @@ namespace Timeline.Services
if (modified)
{
+ user.Version += 1;
await _databaseContext.SaveChangesAsync();
+ //clear cache
+ RemoveCache(user.Id);
}
-
- return PatchUserResult.Success;
}
- public async Task<DeleteUserResult> DeleteUser(string username)
+ public async Task DeleteUser(string username)
{
- var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
- {
- return DeleteUserResult.NotExists;
- }
+ throw new UserNotExistException();
_databaseContext.Users.Remove(user);
await _databaseContext.SaveChangesAsync();
- return DeleteUserResult.Deleted;
+ //clear cache
+ RemoveCache(user.Id);
}
- public async Task<ChangePasswordResult> ChangePassword(string username, string oldPassword, string newPassword)
+ public async Task ChangePassword(string username, string oldPassword, string newPassword)
{
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (oldPassword == null)
+ throw new ArgumentNullException(nameof(oldPassword));
+ if (newPassword == null)
+ throw new ArgumentNullException(nameof(newPassword));
+
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
- return ChangePasswordResult.NotExists;
+ throw new UserNotExistException();
var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword);
if (!verifyResult)
- return ChangePasswordResult.BadOldPassword;
+ throw new BadPasswordException();
user.EncryptedPassword = _passwordService.HashPassword(newPassword);
+ user.Version += 1;
await _databaseContext.SaveChangesAsync();
- return ChangePasswordResult.Success;
- }
-
- public async Task<string> GetAvatarUrl(string username)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
-
- if ((await GetUser(username)) == null)
- return null;
-
- var exists = await _cosService.IsObjectExists("avatar", username);
- if (exists)
- return _cosService.GenerateObjectGetUrl("avatar", username);
- else
- return _cosService.GenerateObjectGetUrl("avatar", "__default");
- }
-
- public async Task<PutAvatarResult> PutAvatar(string username, byte[] data, string mimeType)
- {
- if (username == null)
- throw new ArgumentNullException(nameof(username));
- if (data == null)
- throw new ArgumentNullException(nameof(data));
- if (mimeType == null)
- throw new ArgumentNullException(nameof(mimeType));
-
- if ((await GetUser(username)) == null)
- return PutAvatarResult.UserNotExists;
-
- await _cosService.PutObject("avatar", username, data, mimeType);
- return PutAvatarResult.Success;
+ //clear cache
+ RemoveCache(user.Id);
}
}
}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs
index acabe55c..a6965190 100644
--- a/Timeline/Startup.cs
+++ b/Timeline/Startup.cs
@@ -1,4 +1,3 @@
-using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
@@ -7,11 +6,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.IdentityModel.Tokens;
-using System.Text;
-using System.Threading.Tasks;
+using Timeline.Authenticate;
using Timeline.Configs;
-using Timeline.Formatters;
using Timeline.Models;
using Timeline.Services;
@@ -33,10 +29,7 @@ namespace Timeline
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddMvc(options =>
- {
- options.InputFormatters.Add(new StringInputFormatter());
- }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
+ services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddCors(options =>
{
@@ -52,29 +45,8 @@ namespace Timeline
services.Configure<JwtConfig>(Configuration.GetSection(nameof(JwtConfig)));
var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
- services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
- .AddJwtBearer(o =>
- {
- o.Events = new JwtBearerEvents
- {
- OnMessageReceived = delegate (MessageReceivedContext context)
- {
- context.Request.Query.TryGetValue("token", out var value);
- if (value.Count == 1)
- {
- context.Token = value[0];
- }
- return Task.CompletedTask;
- }
- };
- 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));
- });
+ services.AddAuthentication(AuthConstants.Scheme)
+ .AddScheme<AuthOptions, AuthHandler>(AuthConstants.Scheme, AuthConstants.DisplayName, o => { });
services.AddScoped<IUserService, UserService>();
services.AddScoped<IJwtService, JwtService>();
@@ -96,8 +68,7 @@ namespace Timeline
services.AddHttpClient();
- services.Configure<QCloudCosConfig>(Configuration.GetSection(nameof(QCloudCosConfig)));
- services.AddSingleton<IQCloudCosService, QCloudCosService>();
+ services.AddMemoryCache();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.