aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-08-04 16:54:20 +0800
committer杨宇千 <crupest@outlook.com>2019-08-04 16:54:20 +0800
commit6a8f6e48daa3ec101fa770ba96240f406b436a0b (patch)
treee9120e9537f045d0ba7238c536c6549e9e6c1cf5
parent7f7fdd4d3d72980d575f7e27cea4a2d11c8482a8 (diff)
downloadtimeline-6a8f6e48daa3ec101fa770ba96240f406b436a0b.tar.gz
timeline-6a8f6e48daa3ec101fa770ba96240f406b436a0b.tar.bz2
timeline-6a8f6e48daa3ec101fa770ba96240f406b436a0b.zip
WIP: Need to solve the entity framework problem.
-rw-r--r--Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs4
-rw-r--r--Timeline.Tests/Helpers/TestClock.cs50
-rw-r--r--Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs12
-rw-r--r--Timeline.Tests/Timeline.Tests.csproj45
-rw-r--r--Timeline.Tests/TokenUnitTest.cs312
-rw-r--r--Timeline/Services/JwtService.cs363
-rw-r--r--Timeline/Services/PasswordService.cs432
-rw-r--r--Timeline/Services/UserService.cs10
8 files changed, 624 insertions, 604 deletions
diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs
index 03fb9714..27362ac3 100644
--- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs
+++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs
@@ -10,9 +10,9 @@ namespace Timeline.Tests.Helpers.Authentication
{
private const string CreateTokenUrl = "/token/create";
- public static async Task<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password)
+ public static async Task<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password, double? expireOffset = null)
{
- var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password });
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, ExpireOffset = expireOffset });
var result = JsonConvert.DeserializeObject<CreateTokenResponse>(await response.Content.ReadAsStringAsync());
return result;
}
diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs
index fc200be9..91523f2b 100644
--- a/Timeline.Tests/Helpers/TestClock.cs
+++ b/Timeline.Tests/Helpers/TestClock.cs
@@ -1,25 +1,25 @@
-using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.DependencyInjection;
-using System;
-using Timeline.Services;
-
-namespace Timeline.Tests.Helpers
-{
- public class TestClock : IClock
- {
- DateTime? MockCurrentTime { get; set; } = null;
-
- public DateTime GetCurrentTime()
- {
- return MockCurrentTime.GetValueOrDefault(DateTime.Now);
- }
- }
-
- public static class TestClockWebApplicationFactoryExtensions
- {
- public static TestClock GetTestClock<T>(this WebApplicationFactory<T> factory) where T : class
- {
- return factory.Server.Host.Services.GetRequiredService<IClock>() as TestClock;
- }
- }
-}
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using Timeline.Services;
+
+namespace Timeline.Tests.Helpers
+{
+ public class TestClock : IClock
+ {
+ public DateTime? MockCurrentTime { get; set; } = null;
+
+ public DateTime GetCurrentTime()
+ {
+ return MockCurrentTime.GetValueOrDefault(DateTime.Now);
+ }
+ }
+
+ public static class TestClockWebApplicationFactoryExtensions
+ {
+ public static TestClock GetTestClock<T>(this WebApplicationFactory<T> factory) where T : class
+ {
+ return factory.Server.Host.Services.GetRequiredService<IClock>() as TestClock;
+ }
+ }
+}
diff --git a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs
index aa005ba3..5a1f97d5 100644
--- a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs
+++ b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs
@@ -1,11 +1,11 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Timeline.Models;
-using Timeline.Services;
+using Timeline.Services;
using Xunit.Abstractions;
namespace Timeline.Tests.Helpers
@@ -24,12 +24,12 @@ namespace Timeline.Tests.Helpers
.ConfigureServices(services =>
{
var serviceProvider = new ServiceCollection()
- .AddEntityFrameworkInMemoryDatabase()
+ .AddEntityFrameworkSqlite()
.BuildServiceProvider();
services.AddDbContext<DatabaseContext>(options =>
{
- options.UseInMemoryDatabase("timeline");
+ options.UseSqlite("Data Source=:memory:;"); //TODO! This not work!
options.UseInternalServiceProvider(serviceProvider);
});
@@ -50,8 +50,8 @@ namespace Timeline.Tests.Helpers
}
})
.ConfigureTestServices(services =>
- {
- services.AddSingleton<IClock, TestClock>();
+ {
+ services.AddSingleton<IClock, TestClock>();
});
});
}
diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj
index 820737cc..1a5f2850 100644
--- a/Timeline.Tests/Timeline.Tests.csproj
+++ b/Timeline.Tests/Timeline.Tests.csproj
@@ -1,22 +1,23 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
-
- <PropertyGroup>
- <TargetFramework>netcoreapp2.2</TargetFramework>
- </PropertyGroup>
-
- <ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.App" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.2.0" />
- <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="2.2.0-rtm-35646" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
- <PackageReference Include="xunit" Version="2.4.1" />
- <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
- </PackageReference>
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\Timeline\Timeline.csproj" />
- </ItemGroup>
-</Project>
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp2.2</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.App" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.2.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="2.2.0-rtm-35646" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Timeline\Timeline.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/Timeline.Tests/TokenUnitTest.cs b/Timeline.Tests/TokenUnitTest.cs
index d7df8797..1fe3cff6 100644
--- a/Timeline.Tests/TokenUnitTest.cs
+++ b/Timeline.Tests/TokenUnitTest.cs
@@ -1,147 +1,165 @@
-using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.DependencyInjection;
-using Newtonsoft.Json;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using Timeline.Controllers;
-using Timeline.Entities;
-using Timeline.Entities.Http;
-using Timeline.Models;
-using Timeline.Services;
-using Timeline.Tests.Helpers;
-using Timeline.Tests.Helpers.Authentication;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Timeline.Tests
-{
- public class TokenUnitTest : IClassFixture<WebApplicationFactory<Startup>>
- {
- private const string CreateTokenUrl = "token/create";
- private const string VerifyTokenUrl = "token/verify";
-
- private readonly WebApplicationFactory<Startup> _factory;
-
- public TokenUnitTest(WebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
- {
- _factory = factory.WithTestConfig(outputHelper);
- }
-
- [Fact]
- public async void CreateTokenTest_UserNotExist()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "usernotexist", Password = "???" });
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var body = await response.ReadBodyAsJson<CommonResponse>();
- Assert.Equal(TokenController.ErrorCodes.Create_UserNotExist, body.Code);
- }
- }
-
- [Fact]
- public async void CreateTokenTest_BadPassword()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "???" });
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var body = await response.ReadBodyAsJson<CommonResponse>();
- Assert.Equal(TokenController.ErrorCodes.Create_BadPassword, body.Code);
- }
- }
-
- [Fact]
- public async void CreateTokenTest_BadExpireOffset()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "???", Password = "???", ExpireOffset = -1000 });
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var body = await response.ReadBodyAsJson<CommonResponse>();
- Assert.Equal(TokenController.ErrorCodes.Create_BadExpireOffset, body.Code);
- }
- }
-
- [Fact]
- public async void CreateTokenTest_Success()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "user" });
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var body = await response.ReadBodyAsJson<CreateTokenResponse>();
- Assert.NotEmpty(body.Token);
- Assert.Equal(TestMockUsers.MockUserInfos.Where(u => u.Username == "user").Single(), body.User, UserInfoComparers.EqualityComparer);
- }
- }
-
- [Fact]
- public async void VerifyTokenTest_BadToken()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" });
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var body = await response.ReadBodyAsJson<CommonResponse>();
- Assert.Equal(TokenController.ErrorCodes.Verify_BadToken, body.Code);
- }
- }
-
- [Fact]
- public async void VerifyTokenTest_BadVersion_AND_UserNotExist()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped.
- {
- // create a user for test
- var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
-
- const string username = "verifytokentest0";
- const string password = "12345678";
-
- await userService.PutUser(username, password, false);
-
- // create a token
- var token = (await client.CreateUserTokenAsync(username, password)).Token;
-
- // increase version
- await userService.PatchUser(username, null, null);
-
- // test against bad version
- var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var body = await response.ReadBodyAsJson<CommonResponse>();
- Assert.Equal(TokenController.ErrorCodes.Verify_BadVersion, body.Code);
-
- // create another token
- var token2 = (await client.CreateUserTokenAsync(username, password)).Token;
-
- // delete user
- await userService.DeleteUser(username);
-
- // test against user not exist
- var response2 = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
- Assert.Equal(HttpStatusCode.BadRequest, response2.StatusCode);
- var body2 = await response2.ReadBodyAsJson<CommonResponse>();
- Assert.Equal(TokenController.ErrorCodes.Verify_UserNotExist, body2.Code);
- }
- }
- }
-
- [Fact]
- public async void VerifyTokenTest_Success()
- {
- using (var client = _factory.CreateDefaultClient())
- {
- var createTokenResult = await client.CreateUserTokenAsync("admin", "admin");
- var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = createTokenResult.Token });
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
- var body = JsonConvert.DeserializeObject<VerifyTokenResponse>(await response.Content.ReadAsStringAsync());
- Assert.Equal(TestMockUsers.MockUserInfos.Where(u => u.Username == "user").Single(), body.User, UserInfoComparers.EqualityComparer);
- }
- }
- }
-}
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using Timeline.Controllers;
+using Timeline.Entities;
+using Timeline.Entities.Http;
+using Timeline.Models;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Helpers.Authentication;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests
+{
+ public class TokenUnitTest : IClassFixture<WebApplicationFactory<Startup>>
+ {
+ private const string CreateTokenUrl = "token/create";
+ private const string VerifyTokenUrl = "token/verify";
+
+ private readonly WebApplicationFactory<Startup> _factory;
+
+ public TokenUnitTest(WebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
+ {
+ _factory = factory.WithTestConfig(outputHelper);
+ }
+
+ [Fact]
+ public async void CreateTokenTest_UserNotExist()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "usernotexist", Password = "???" });
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var body = await response.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Create_UserNotExist, body.Code);
+ }
+ }
+
+ [Fact]
+ public async void CreateTokenTest_BadPassword()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "???" });
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var body = await response.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Create_BadPassword, body.Code);
+ }
+ }
+
+ [Fact]
+ public async void CreateTokenTest_BadExpireOffset()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "???", Password = "???", ExpireOffset = -1000 });
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var body = await response.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Create_BadExpireOffset, body.Code);
+ }
+ }
+
+ [Fact]
+ public async void CreateTokenTest_Success()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "user" });
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.ReadBodyAsJson<CreateTokenResponse>();
+ Assert.NotEmpty(body.Token);
+ Assert.Equal(TestMockUsers.MockUserInfos.Where(u => u.Username == "user").Single(), body.User, UserInfoComparers.EqualityComparer);
+ }
+ }
+
+ [Fact]
+ public async void VerifyTokenTest_BadToken()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" });
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var body = await response.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Verify_BadToken, body.Code);
+ }
+ }
+
+ [Fact]
+ public async void VerifyTokenTest_BadVersion_AND_UserNotExist()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ using (var scope = _factory.Server.Host.Services.CreateScope()) // UserService is scoped.
+ {
+ // create a user for test
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+
+ const string username = "verifytokentest0";
+ const string password = "12345678";
+
+ await userService.PutUser(username, password, false);
+
+ // create a token
+ var token = (await client.CreateUserTokenAsync(username, password)).Token;
+
+ // increase version
+ await userService.PatchUser(username, null, null);
+
+ // test against bad version
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ var body = await response.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Verify_BadVersion, body.Code);
+
+ // create another token
+ var token2 = (await client.CreateUserTokenAsync(username, password)).Token;
+
+ // delete user
+ await userService.DeleteUser(username);
+
+ // test against user not exist
+ var response2 = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
+ Assert.Equal(HttpStatusCode.BadRequest, response2.StatusCode);
+ var body2 = await response2.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Verify_UserNotExist, body2.Code);
+ }
+ }
+ }
+
+ [Fact]
+ public async void VerifyTokenTest_Expired()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ // I can only control the token expired time but not current time
+ // because verify logic is encapsuled in other library.
+ var mockClock = _factory.GetTestClock();
+ mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2);
+ var token = (await client.CreateUserTokenAsync("user", "user", 1)).Token;
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token });
+ var body = await response.ReadBodyAsJson<CommonResponse>();
+ Assert.Equal(TokenController.ErrorCodes.Verify_Expired, body.Code);
+ mockClock.MockCurrentTime = null;
+ }
+ }
+
+ [Fact]
+ public async void VerifyTokenTest_Success()
+ {
+ using (var client = _factory.CreateDefaultClient())
+ {
+ var createTokenResult = await client.CreateUserTokenAsync("user", "user");
+ var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = createTokenResult.Token });
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = JsonConvert.DeserializeObject<VerifyTokenResponse>(await response.Content.ReadAsStringAsync());
+ Assert.Equal(TestMockUsers.MockUserInfos.Where(u => u.Username == "user").Single(), body.User, UserInfoComparers.EqualityComparer);
+ }
+ }
+ }
+}
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs
index 52e892f6..94afe745 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/JwtService.cs
@@ -1,181 +1,182 @@
-using Microsoft.Extensions.Options;
-using Microsoft.IdentityModel.Tokens;
-using System;
-using System.IdentityModel.Tokens.Jwt;
-using System.Security.Claims;
-using System.Text;
-using Timeline.Configs;
-
-namespace Timeline.Services
-{
- public class TokenInfo
- {
- public long Id { get; set; }
- public long Version { get; set; }
- }
-
- [Serializable]
- public class JwtTokenVerifyException : Exception
- {
- public static class ErrorCodes
- {
- // Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server.
-
- public const int Others = -1001;
- public const int NoIdClaim = -1002;
- public const int IdClaimBadFormat = -1003;
- public const int NoVersionClaim = -1004;
- public const int VersionClaimBadFormat = -1005;
-
- /// <summary>
- /// Corresponds to <see cref="SecurityTokenExpiredException"/>.
- /// </summary>
- public const int Expired = -2001;
- }
-
- public JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; }
- public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; }
- public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; }
- public JwtTokenVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; }
- protected JwtTokenVerifyException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public int ErrorCode { get; private set; }
-
- private static string GetErrorMessage(int errorCode)
- {
- switch (errorCode)
- {
- case ErrorCodes.Others:
- return "Uncommon error, see inner exception for more information.";
- case ErrorCodes.NoIdClaim:
- return "Id claim does not exist.";
- case ErrorCodes.IdClaimBadFormat:
- return "Id claim is not a number.";
- case ErrorCodes.NoVersionClaim:
- return "Version claim does not exist.";
- case ErrorCodes.VersionClaimBadFormat:
- return "Version claim is not a number";
- case ErrorCodes.Expired:
- return "Token is expired.";
- default:
- return "Unknown error code.";
- }
- }
- }
-
- public interface IJwtService
- {
- /// <summary>
- /// 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>
- /// <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 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 IClock _clock;
-
- public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
- {
- _jwtConfig = jwtConfig;
- _clock = clock;
- }
-
- public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null)
- {
- if (tokenInfo == null)
- throw new ArgumentNullException(nameof(tokenInfo));
-
- var config = _jwtConfig.CurrentValue;
-
- var identity = new ClaimsIdentity();
- identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64));
- identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64));
-
- var tokenDescriptor = new SecurityTokenDescriptor()
- {
- Subject = identity,
- Issuer = config.Issuer,
- Audience = config.Audience,
- SigningCredentials = new SigningCredentials(
- new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384),
- IssuedAt = _clock.GetCurrentTime(),
- Expires = expires.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset))
- };
-
- var token = _tokenHandler.CreateToken(tokenDescriptor);
- var tokenString = _tokenHandler.WriteToken(token);
-
- return tokenString;
- }
-
-
- public TokenInfo VerifyJwtToken(string token)
- {
- if (token == null)
- throw new ArgumentNullException(nameof(token));
-
- var config = _jwtConfig.CurrentValue;
- try
- {
- var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters
- {
- ValidateIssuer = true,
- ValidateAudience = true,
- ValidateIssuerSigningKey = true,
- ValidateLifetime = true,
- ValidIssuer = config.Issuer,
- ValidAudience = config.Audience,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
- }, out _);
-
- var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
- if (idClaim == null)
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoIdClaim);
- if (!long.TryParse(idClaim, out var id))
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat);
-
- var versionClaim = principal.FindFirstValue(VersionClaimType);
- if (versionClaim == null)
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoVersionClaim);
- if (!long.TryParse(versionClaim, out var version))
- throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.VersionClaimBadFormat);
-
- return new TokenInfo
- {
- Id = id,
- Version = version
- };
- }
- catch (SecurityTokenExpiredException e)
- {
- throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Expired);
- }
- catch (Exception e)
- {
- throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Others);
- }
- }
- }
-}
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using Timeline.Configs;
+
+namespace Timeline.Services
+{
+ public class TokenInfo
+ {
+ public long Id { get; set; }
+ public long Version { get; set; }
+ }
+
+ [Serializable]
+ public class JwtTokenVerifyException : Exception
+ {
+ public static class ErrorCodes
+ {
+ // Codes in -1000 ~ -1999 usually means the user provides a token that is not created by this server.
+
+ public const int Others = -1001;
+ public const int NoIdClaim = -1002;
+ public const int IdClaimBadFormat = -1003;
+ public const int NoVersionClaim = -1004;
+ public const int VersionClaimBadFormat = -1005;
+
+ /// <summary>
+ /// Corresponds to <see cref="SecurityTokenExpiredException"/>.
+ /// </summary>
+ public const int Expired = -2001;
+ }
+
+ public JwtTokenVerifyException(int code) : base(GetErrorMessage(code)) { ErrorCode = code; }
+ public JwtTokenVerifyException(string message, int code) : base(message) { ErrorCode = code; }
+ public JwtTokenVerifyException(Exception inner, int code) : base(GetErrorMessage(code), inner) { ErrorCode = code; }
+ public JwtTokenVerifyException(string message, Exception inner, int code) : base(message, inner) { ErrorCode = code; }
+ protected JwtTokenVerifyException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public int ErrorCode { get; private set; }
+
+ private static string GetErrorMessage(int errorCode)
+ {
+ switch (errorCode)
+ {
+ case ErrorCodes.Others:
+ return "Uncommon error, see inner exception for more information.";
+ case ErrorCodes.NoIdClaim:
+ return "Id claim does not exist.";
+ case ErrorCodes.IdClaimBadFormat:
+ return "Id claim is not a number.";
+ case ErrorCodes.NoVersionClaim:
+ return "Version claim does not exist.";
+ case ErrorCodes.VersionClaimBadFormat:
+ return "Version claim is not a number";
+ case ErrorCodes.Expired:
+ return "Token is expired.";
+ default:
+ return "Unknown error code.";
+ }
+ }
+ }
+
+ public interface IJwtService
+ {
+ /// <summary>
+ /// 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>
+ /// <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 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 IClock _clock;
+
+ public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, IClock clock)
+ {
+ _jwtConfig = jwtConfig;
+ _clock = clock;
+ }
+
+ public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null)
+ {
+ if (tokenInfo == null)
+ throw new ArgumentNullException(nameof(tokenInfo));
+
+ var config = _jwtConfig.CurrentValue;
+
+ var identity = new ClaimsIdentity();
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64));
+ identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64));
+
+ var tokenDescriptor = new SecurityTokenDescriptor()
+ {
+ Subject = identity,
+ Issuer = config.Issuer,
+ Audience = config.Audience,
+ SigningCredentials = new SigningCredentials(
+ new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384),
+ IssuedAt = _clock.GetCurrentTime(),
+ Expires = expires.GetValueOrDefault(_clock.GetCurrentTime().AddSeconds(config.DefaultExpireOffset)),
+ NotBefore = _clock.GetCurrentTime() // I must explicitly set this or it will use the current time by default and mock is not work in which case test will not pass.
+ };
+
+ var token = _tokenHandler.CreateToken(tokenDescriptor);
+ var tokenString = _tokenHandler.WriteToken(token);
+
+ return tokenString;
+ }
+
+
+ public TokenInfo VerifyJwtToken(string token)
+ {
+ if (token == null)
+ throw new ArgumentNullException(nameof(token));
+
+ var config = _jwtConfig.CurrentValue;
+ try
+ {
+ var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateIssuerSigningKey = true,
+ ValidateLifetime = true,
+ ValidIssuer = config.Issuer,
+ ValidAudience = config.Audience,
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
+ }, out _);
+
+ var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
+ if (idClaim == null)
+ throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoIdClaim);
+ if (!long.TryParse(idClaim, out var id))
+ throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.IdClaimBadFormat);
+
+ var versionClaim = principal.FindFirstValue(VersionClaimType);
+ if (versionClaim == null)
+ throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.NoVersionClaim);
+ if (!long.TryParse(versionClaim, out var version))
+ throw new JwtTokenVerifyException(JwtTokenVerifyException.ErrorCodes.VersionClaimBadFormat);
+
+ return new TokenInfo
+ {
+ Id = id,
+ Version = version
+ };
+ }
+ catch (SecurityTokenExpiredException e)
+ {
+ throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Expired);
+ }
+ catch (Exception e)
+ {
+ throw new JwtTokenVerifyException(e, JwtTokenVerifyException.ErrorCodes.Others);
+ }
+ }
+ }
+}
diff --git a/Timeline/Services/PasswordService.cs b/Timeline/Services/PasswordService.cs
index d114bb26..8c67d046 100644
--- a/Timeline/Services/PasswordService.cs
+++ b/Timeline/Services/PasswordService.cs
@@ -1,216 +1,216 @@
-using Microsoft.AspNetCore.Cryptography.KeyDerivation;
-using System;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-
-namespace Timeline.Services
-{
- /// <summary>
- /// Hashed password is of bad format.
- /// </summary>
- /// <seealso cref="IPasswordService.VerifyPassword(string, string)"/>
- [Serializable]
- public class HashedPasswordBadFromatException : Exception
- {
- public HashedPasswordBadFromatException(string hashedPassword, string message) : base(message) { HashedPassword = hashedPassword; }
- public HashedPasswordBadFromatException(string hashedPassword, string message, Exception inner) : base(message, inner) { HashedPassword = hashedPassword; }
- protected HashedPasswordBadFromatException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
-
- public string HashedPassword { get; private set; }
- }
-
- public interface IPasswordService
- {
- /// <summary>
- /// Hash a password.
- /// </summary>
- /// <param name="password">The password to hash.</param>
- /// <returns>A hashed representation of the supplied <paramref name="password"/>.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="password"/> is null.</exception>
- string HashPassword(string password);
-
- /// <summary>
- /// Verify whether the password fits into the hashed one.
- ///
- /// Usually you only need to check the returned bool value.
- /// Catching <see cref="HashedPasswordBadFromatException"/> usually is not necessary.
- /// Because if your program logic is right and always call <see cref="HashPassword(string)"/>
- /// and <see cref="VerifyPassword(string, string)"/> in pair, this exception will never be thrown.
- /// A thrown one usually means the data you saved is corupted, which is a critical problem.
- /// </summary>
- /// <param name="hashedPassword">The hashed password.</param>
- /// <param name="providedPassword">The password supplied for comparison.</param>
- /// <returns>True indicating password is right. Otherwise false.</returns>
- /// <exception cref="ArgumentNullException">Thrown when <paramref name="hashedPassword"/> or <paramref name="providedPassword"/> is null.</exception>
- /// <exception cref="HashedPasswordBadFromatException">Thrown when the hashed password is of bad format.</exception>
- bool VerifyPassword(string hashedPassword, string providedPassword);
- }
-
- /// <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.
- /// Remove configuration options.
- /// Remove user related parts.
- /// Change the exceptions.
- /// </summary>
- public class PasswordService : IPasswordService
- {
- /* =======================
- * HASHED PASSWORD FORMATS
- * =======================
- *
- * Version 3:
- * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
- * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
- * (All UInt32s are stored big-endian.)
- */
-
- private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
-
- public PasswordService()
- {
- }
-
- // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
- [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
- private static bool ByteArraysEqual(byte[] a, byte[] b)
- {
- if (a == null && b == null)
- {
- return true;
- }
- if (a == null || b == null || a.Length != b.Length)
- {
- return false;
- }
- var areSame = true;
- for (var i = 0; i < a.Length; i++)
- {
- areSame &= (a[i] == b[i]);
- }
- return areSame;
- }
-
- public string HashPassword(string password)
- {
- if (password == null)
- throw new ArgumentNullException(nameof(password));
- return Convert.ToBase64String(HashPasswordV3(password, _rng));
- }
-
- private byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
- {
- return HashPasswordV3(password, rng,
- prf: KeyDerivationPrf.HMACSHA256,
- iterCount: 10000,
- saltSize: 128 / 8,
- numBytesRequested: 256 / 8);
- }
-
- private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
- {
- // Produce a version 3 (see comment above) text hash.
- byte[] salt = new byte[saltSize];
- rng.GetBytes(salt);
- byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
-
- var outputBytes = new byte[13 + salt.Length + subkey.Length];
- outputBytes[0] = 0x01; // format marker
- WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
- WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
- WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
- Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
- Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
- return outputBytes;
- }
-
- public bool VerifyPassword(string hashedPassword, string providedPassword)
- {
- if (hashedPassword == null)
- throw new ArgumentNullException(nameof(hashedPassword));
- if (providedPassword == null)
- throw new ArgumentNullException(nameof(providedPassword));
-
- byte[] decodedHashedPassword;
- try
- {
- decodedHashedPassword = Convert.FromBase64String(hashedPassword);
- }
- catch (FormatException e)
- {
- throw new HashedPasswordBadFromatException(hashedPassword, "Not of valid base64 format. See inner exception.", e);
- }
-
- // read the format marker from the hashed password
- if (decodedHashedPassword.Length == 0)
- {
- throw new HashedPasswordBadFromatException(hashedPassword, "Decoded hashed password is of length 0.");
- }
- switch (decodedHashedPassword[0])
- {
- case 0x01:
- return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword);
-
- default:
- throw new HashedPasswordBadFromatException(hashedPassword, "Unknown format marker.");
- }
- }
-
- private bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString)
- {
- try
- {
- // Read header information
- KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
- int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
- int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
-
- // Read the salt: must be >= 128 bits
- if (saltLength < 128 / 8)
- {
- throw new HashedPasswordBadFromatException(hashedPasswordString, "Salt length < 128 bits.");
- }
- byte[] salt = new byte[saltLength];
- Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
-
- // Read the subkey (the rest of the payload): must be >= 128 bits
- int subkeyLength = hashedPassword.Length - 13 - salt.Length;
- if (subkeyLength < 128 / 8)
- {
- throw new HashedPasswordBadFromatException(hashedPasswordString, "Subkey length < 128 bits.");
- }
- byte[] expectedSubkey = new byte[subkeyLength];
- Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
-
- // Hash the incoming password and verify it
- byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
- return ByteArraysEqual(actualSubkey, expectedSubkey);
- }
- catch (Exception e)
- {
- // This should never occur except in the case of a malformed payload, where
- // we might go off the end of the array. Regardless, a malformed payload
- // implies verification failed.
- throw new HashedPasswordBadFromatException(hashedPasswordString, "See inner exception.", e);
- }
- }
-
- private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
- {
- return ((uint)(buffer[offset + 0]) << 24)
- | ((uint)(buffer[offset + 1]) << 16)
- | ((uint)(buffer[offset + 2]) << 8)
- | ((uint)(buffer[offset + 3]));
- }
-
- private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
- {
- buffer[offset + 0] = (byte)(value >> 24);
- buffer[offset + 1] = (byte)(value >> 16);
- buffer[offset + 2] = (byte)(value >> 8);
- buffer[offset + 3] = (byte)(value >> 0);
- }
- }
-}
+using Microsoft.AspNetCore.Cryptography.KeyDerivation;
+using System;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+namespace Timeline.Services
+{
+ /// <summary>
+ /// Hashed password is of bad format.
+ /// </summary>
+ /// <seealso cref="IPasswordService.VerifyPassword(string, string)"/>
+ [Serializable]
+ public class HashedPasswordBadFromatException : Exception
+ {
+ public HashedPasswordBadFromatException(string hashedPassword, string message) : base(message) { HashedPassword = hashedPassword; }
+ public HashedPasswordBadFromatException(string hashedPassword, string message, Exception inner) : base(message, inner) { HashedPassword = hashedPassword; }
+ protected HashedPasswordBadFromatException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ public string HashedPassword { get; private set; }
+ }
+
+ public interface IPasswordService
+ {
+ /// <summary>
+ /// Hash a password.
+ /// </summary>
+ /// <param name="password">The password to hash.</param>
+ /// <returns>A hashed representation of the supplied <paramref name="password"/>.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="password"/> is null.</exception>
+ string HashPassword(string password);
+
+ /// <summary>
+ /// Verify whether the password fits into the hashed one.
+ ///
+ /// Usually you only need to check the returned bool value.
+ /// Catching <see cref="HashedPasswordBadFromatException"/> usually is not necessary.
+ /// Because if your program logic is right and always call <see cref="HashPassword(string)"/>
+ /// and <see cref="VerifyPassword(string, string)"/> in pair, this exception will never be thrown.
+ /// A thrown one usually means the data you saved is corupted, which is a critical problem.
+ /// </summary>
+ /// <param name="hashedPassword">The hashed password.</param>
+ /// <param name="providedPassword">The password supplied for comparison.</param>
+ /// <returns>True indicating password is right. Otherwise false.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="hashedPassword"/> or <paramref name="providedPassword"/> is null.</exception>
+ /// <exception cref="HashedPasswordBadFromatException">Thrown when the hashed password is of bad format.</exception>
+ bool VerifyPassword(string hashedPassword, string providedPassword);
+ }
+
+ /// <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.
+ /// Remove configuration options.
+ /// Remove user related parts.
+ /// Change the exceptions.
+ /// </summary>
+ public class PasswordService : IPasswordService
+ {
+ /* =======================
+ * HASHED PASSWORD FORMATS
+ * =======================
+ *
+ * Version 3:
+ * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
+ * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
+ * (All UInt32s are stored big-endian.)
+ */
+
+ private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
+
+ public PasswordService()
+ {
+ }
+
+ // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static bool ByteArraysEqual(byte[] a, byte[] b)
+ {
+ if (a == null && b == null)
+ {
+ return true;
+ }
+ if (a == null || b == null || a.Length != b.Length)
+ {
+ return false;
+ }
+ var areSame = true;
+ for (var i = 0; i < a.Length; i++)
+ {
+ areSame &= (a[i] == b[i]);
+ }
+ return areSame;
+ }
+
+ public string HashPassword(string password)
+ {
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+ return Convert.ToBase64String(HashPasswordV3(password, _rng));
+ }
+
+ private byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
+ {
+ return HashPasswordV3(password, rng,
+ prf: KeyDerivationPrf.HMACSHA256,
+ iterCount: 10000,
+ saltSize: 128 / 8,
+ numBytesRequested: 256 / 8);
+ }
+
+ private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
+ {
+ // Produce a version 3 (see comment above) text hash.
+ byte[] salt = new byte[saltSize];
+ rng.GetBytes(salt);
+ byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
+
+ var outputBytes = new byte[13 + salt.Length + subkey.Length];
+ outputBytes[0] = 0x01; // format marker
+ WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
+ WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
+ WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
+ Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
+ Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
+ return outputBytes;
+ }
+
+ public bool VerifyPassword(string hashedPassword, string providedPassword)
+ {
+ if (hashedPassword == null)
+ throw new ArgumentNullException(nameof(hashedPassword));
+ if (providedPassword == null)
+ throw new ArgumentNullException(nameof(providedPassword));
+
+ byte[] decodedHashedPassword;
+ try
+ {
+ decodedHashedPassword = Convert.FromBase64String(hashedPassword);
+ }
+ catch (FormatException e)
+ {
+ throw new HashedPasswordBadFromatException(hashedPassword, "Not of valid base64 format. See inner exception.", e);
+ }
+
+ // read the format marker from the hashed password
+ if (decodedHashedPassword.Length == 0)
+ {
+ throw new HashedPasswordBadFromatException(hashedPassword, "Decoded hashed password is of length 0.");
+ }
+ switch (decodedHashedPassword[0])
+ {
+ case 0x01:
+ return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword);
+
+ default:
+ throw new HashedPasswordBadFromatException(hashedPassword, "Unknown format marker.");
+ }
+ }
+
+ private bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString)
+ {
+ try
+ {
+ // Read header information
+ KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
+ int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
+ int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
+
+ // Read the salt: must be >= 128 bits
+ if (saltLength < 128 / 8)
+ {
+ throw new HashedPasswordBadFromatException(hashedPasswordString, "Salt length < 128 bits.");
+ }
+ byte[] salt = new byte[saltLength];
+ Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
+
+ // Read the subkey (the rest of the payload): must be >= 128 bits
+ int subkeyLength = hashedPassword.Length - 13 - salt.Length;
+ if (subkeyLength < 128 / 8)
+ {
+ throw new HashedPasswordBadFromatException(hashedPasswordString, "Subkey length < 128 bits.");
+ }
+ byte[] expectedSubkey = new byte[subkeyLength];
+ Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
+
+ // Hash the incoming password and verify it
+ byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
+ return ByteArraysEqual(actualSubkey, expectedSubkey);
+ }
+ catch (Exception e)
+ {
+ // This should never occur except in the case of a malformed payload, where
+ // we might go off the end of the array. Regardless, a malformed payload
+ // implies verification failed.
+ throw new HashedPasswordBadFromatException(hashedPasswordString, "See inner exception.", e);
+ }
+ }
+
+ private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
+ {
+ return ((uint)(buffer[offset + 0]) << 24)
+ | ((uint)(buffer[offset + 1]) << 16)
+ | ((uint)(buffer[offset + 2]) << 8)
+ | ((uint)(buffer[offset + 3]));
+ }
+
+ private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
+ {
+ buffer[offset + 0] = (byte)(value >> 24);
+ buffer[offset + 1] = (byte)(value >> 16);
+ buffer[offset + 2] = (byte)(value >> 8);
+ buffer[offset + 3] = (byte)(value >> 0);
+ }
+ }
+}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 65ac98d3..0d6934ff 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -319,11 +319,11 @@ namespace Timeline.Services
if (administrator != null)
{
user.RoleString = IsAdminToRoleString(administrator.Value);
- }
-
- user.Version += 1;
- await _databaseContext.SaveChangesAsync();
- //clear cache
+ }
+
+ user.Version += 1;
+ await _databaseContext.SaveChangesAsync();
+ //clear cache
RemoveCache(user.Id);
}