Add auth module
This commit is contained in:
parent
3e1555d052
commit
4e4a720aa4
|
@ -0,0 +1,47 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<LangVersion>10</LangVersion>
|
||||||
|
<RootNamespace>BlueWest.WebApi</RootNamespace>
|
||||||
|
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
|
||||||
|
<PublishDependencyDocumentationFiles>true</PublishDependencyDocumentationFiles>
|
||||||
|
<AnalysisLevel>preview</AnalysisLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Grpc.AspNetCore" Version="2.49.0-pre1" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="6.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authorization.Policy" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.8">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.2-mauipre.1.22102.15" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="6.0.2-mauipre.1.22054.8" />
|
||||||
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\BlueWest.Data.Application\BlueWest.Data.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\BlueWest.Domain\BlueWest.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,5 @@
|
||||||
|
namespace BlueWest.Data.Auth;
|
||||||
|
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data.Application;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Methods for handling session cache data.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISessionCache : IHostedService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a Session Token by Id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tokenId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<SessionToken> GetSessionTokenByIdAsync(string tokenId);
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new session token
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
Task AddSessionToken(SessionToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check for validity of the session
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionTokenId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> IsSessionValidAsync(string sessionTokenId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the session is valid
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionTokenId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool IsSessionValid(string sessionTokenId);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save Cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task SaveAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save Cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
void Save();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
// from: https://github.com/dotnet/aspnetcore/tree/main/src/Identity/samples/IdentitySample.Mvc/Models/AccountViewModels
|
||||||
|
/// <summary>
|
||||||
|
/// Login Request adata
|
||||||
|
/// </summary>
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Email
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Password
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Uuid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Uuid for this login request
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetUuid()
|
||||||
|
{
|
||||||
|
return $"{Uuid}|{Email}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class RegisterViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Email
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
[Display(Name = "Email")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Password
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Password")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Username
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ConfirmPassword
|
||||||
|
/// </summary>
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Confirm password")]
|
||||||
|
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||||
|
public string ConfirmPassword { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ConfirmPassword
|
||||||
|
/// </summary>
|
||||||
|
[DataType(DataType.PhoneNumber)]
|
||||||
|
[Display(Name = "Phone Number")]
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert RegisterViewModel to ApplicationUser
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public ApplicationUser ToUser()
|
||||||
|
{
|
||||||
|
var newUser = new ApplicationUser();
|
||||||
|
newUser.Email = Email;
|
||||||
|
newUser.PasswordHash = Password;
|
||||||
|
newUser.UserName = Username;
|
||||||
|
newUser.PhoneNumber = PhoneNumber;
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi
|
||||||
|
{
|
||||||
|
internal static class SessionConstants
|
||||||
|
{
|
||||||
|
|
||||||
|
public static TimeSpan DefaultSessionMaxAge = TimeSpan.FromHours(24);
|
||||||
|
public const string ApiNamePolicy = "ApiUser";
|
||||||
|
public const string SessionTokenHeaderName = "x-bw2-auth";
|
||||||
|
public const string CookieDomain = "http://localhost:5173";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Cryptography;
|
||||||
|
using BlueWest.Data.Application;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User Manager Object
|
||||||
|
/// </summary>
|
||||||
|
internal class ApplicationUserManager : UserManager<ApplicationUser>, IUserManager
|
||||||
|
{
|
||||||
|
private readonly IHasher _hasher;
|
||||||
|
private readonly UserRepository _usersRepo;
|
||||||
|
public ApplicationUserManager(
|
||||||
|
UserRepository store,
|
||||||
|
IOptions<IdentityOptions> optionsAccessor,
|
||||||
|
IHasher passwordHasher,
|
||||||
|
IEnumerable<IUserValidator<ApplicationUser>> userValidators,
|
||||||
|
IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators,
|
||||||
|
ILookupNormalizer keyNormalizer,
|
||||||
|
IdentityErrorDescriber errors,
|
||||||
|
IServiceProvider services,
|
||||||
|
ILogger<UserManager<ApplicationUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||||
|
{
|
||||||
|
_hasher = passwordHasher;
|
||||||
|
_usersRepo = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var passwordStore = GetPasswordStore();
|
||||||
|
|
||||||
|
var result = await VerifyPasswordAsync(passwordStore, user, password);
|
||||||
|
if (result == PasswordVerificationResult.SuccessRehashNeeded)
|
||||||
|
{
|
||||||
|
//Remove the IPasswordStore parameter so we can call the protected, not private, method
|
||||||
|
await UpdatePasswordHash(user, password, validatePassword: false);
|
||||||
|
await UpdateUserAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = result != PasswordVerificationResult.Failed;
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
var userId = user != null ? GetUserIdAsync(user).Result : "(null)";
|
||||||
|
Logger.LogWarning(0, "Invalid password for user {userId}.", userId);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected override async Task<PasswordVerificationResult> VerifyPasswordAsync(IUserPasswordStore<ApplicationUser> store, ApplicationUser user, string password)
|
||||||
|
{
|
||||||
|
string existingHash;
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
existingHash = await store.GetPasswordHashAsync(user, CancellationToken);
|
||||||
|
else
|
||||||
|
existingHash = "not a real hash";
|
||||||
|
|
||||||
|
if (existingHash == null)
|
||||||
|
{
|
||||||
|
return PasswordVerificationResult.Failed;
|
||||||
|
}
|
||||||
|
return PasswordHasher.VerifyHashedPassword(user, existingHash, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task<IdentityResult> ChangePasswordAsync(ApplicationUser user, string currentPassword, string newPassword)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var passwordStore = GetPasswordStore();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await VerifyPasswordAsync(passwordStore, user, currentPassword) != PasswordVerificationResult.Failed)
|
||||||
|
{
|
||||||
|
var result = await UpdatePasswordHash(user, newPassword, validatePassword: false);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await UpdateUserAsync(user);
|
||||||
|
}
|
||||||
|
Logger.LogWarning(2, "Change password failed for user {userId}.", await GetUserIdAsync(user));
|
||||||
|
return IdentityResult.Failed(ErrorDescriber.PasswordMismatch());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<IdentityResult> SetAuthenticationTokenAsync(ApplicationUser user, string loginProvider, string tokenName, string tokenValue)
|
||||||
|
{
|
||||||
|
return base.SetAuthenticationTokenAsync(user, loginProvider, tokenName, tokenValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUserPasswordStore<ApplicationUser> GetPasswordStore()
|
||||||
|
{
|
||||||
|
if (Store is IUserPasswordStore<ApplicationUser> passwordStore)
|
||||||
|
{
|
||||||
|
return passwordStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data.Application;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public static class AuthConsts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper object to return a negative callback
|
||||||
|
/// </summary>
|
||||||
|
public static (bool, SessionTokenUnique, ClaimsIdentity) NegativeToken => (false, null, null);
|
||||||
|
|
||||||
|
public static (bool, SessionTokenUnique, ClaimsIdentity) OkAuth(SessionTokenUnique sessionTokenUnique, ClaimsIdentity claimsIdentity, bool success = true) => (success, sessionTokenUnique, claimsIdentity);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Cryptography;
|
||||||
|
using BlueWest.Data.Application;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using static BlueWest.WebApi.Context.Users.AuthConsts;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
internal class AuthManager : IAuthManager
|
||||||
|
{
|
||||||
|
private readonly ApplicationUserManager _userManager;
|
||||||
|
private readonly IHasher _hasher;
|
||||||
|
private readonly IJwtFactory _jwtFactory;
|
||||||
|
private readonly ISessionCache _sessionCache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auth manager constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager"></param>
|
||||||
|
/// <param name="hasher"></param>
|
||||||
|
/// <param name="jwtFactory"></param>
|
||||||
|
/// <param name="sessionCache"></param>
|
||||||
|
public AuthManager(
|
||||||
|
ApplicationUserManager userManager,
|
||||||
|
IHasher hasher,
|
||||||
|
IJwtFactory jwtFactory,
|
||||||
|
ISessionCache sessionCache)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_hasher = hasher;
|
||||||
|
_jwtFactory = jwtFactory;
|
||||||
|
_sessionCache = sessionCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SessionToken> GetSessionToken(LoginRequest loginRequest)
|
||||||
|
{
|
||||||
|
var uuid = loginRequest.GetUuid();
|
||||||
|
var hashUuid = GetHashFromUuid(uuid);
|
||||||
|
var sessionToken = await _sessionCache.GetSessionTokenByIdAsync(hashUuid);
|
||||||
|
return sessionToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetHashFromUuid(string uuid)
|
||||||
|
{
|
||||||
|
return _hasher.CreateHash(uuid, BaseCryptoItem.HashAlgorithm.SHA2_512);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SessionToken GetNewSessionToken(LoginRequest loginRequest, ApplicationUser user, string token)
|
||||||
|
{
|
||||||
|
long timeNow = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
var newToken = new SessionToken
|
||||||
|
{
|
||||||
|
Id = GetHashFromUuid(loginRequest.GetUuid()),
|
||||||
|
UserId = user.Id,
|
||||||
|
CreatedDate = timeNow,
|
||||||
|
IsValid = true,
|
||||||
|
ValidFor = SessionConstants.DefaultSessionMaxAge.Milliseconds,
|
||||||
|
AccessToken = token
|
||||||
|
};
|
||||||
|
|
||||||
|
return newToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SessionTokenIsValid(SessionToken token)
|
||||||
|
{
|
||||||
|
var hasChanges = token.Validate();
|
||||||
|
|
||||||
|
if (hasChanges)
|
||||||
|
{
|
||||||
|
_sessionCache.SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.IsValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequest(LoginRequest loginRequest)
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(loginRequest.Email);
|
||||||
|
|
||||||
|
if (user == null) return NegativeToken;
|
||||||
|
|
||||||
|
if (!await _userManager.CheckPasswordAsync(user, loginRequest.Password)) return NegativeToken;
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
|
||||||
|
identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
|
||||||
|
|
||||||
|
var sessionToken = await GetSessionToken(loginRequest);
|
||||||
|
|
||||||
|
if (sessionToken == null || !SessionTokenIsValid(sessionToken))
|
||||||
|
{
|
||||||
|
var (success, bearerToken) = await GenerateBearerToken(identity, user);
|
||||||
|
var newSessionToken = GetNewSessionToken(loginRequest, user, bearerToken);
|
||||||
|
await _sessionCache.AddSessionToken(newSessionToken);
|
||||||
|
var tokenUnique = new SessionTokenUnique(newSessionToken);
|
||||||
|
|
||||||
|
return OkAuth(tokenUnique, identity, success);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = new SessionTokenUnique(sessionToken);
|
||||||
|
return OkAuth(response, identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool, string)> GenerateBearerToken(ClaimsIdentity identity, ApplicationUser user)
|
||||||
|
{
|
||||||
|
var jwtToken = await _jwtFactory.GenerateEncodedToken(user.Id, user.UserName);
|
||||||
|
var completed = await _userManager.SetAuthenticationTokenAsync(user, SessionConstants.ApiNamePolicy,
|
||||||
|
SessionConstants.ApiNamePolicy, jwtToken.Token);
|
||||||
|
return (completed == IdentityResult.Success, jwtToken.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<bool> VerifyLoginByEmailAsync(string email, string password)
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(email);
|
||||||
|
return user != null && await _userManager.CheckPasswordAsync(user, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IdentityResult> CreateUserAsync(RegisterViewModel userSignupDto)
|
||||||
|
{
|
||||||
|
userSignupDto.Password = _hasher.CreateHash(userSignupDto.Password, BaseCryptoItem.HashAlgorithm.SHA3_512);;
|
||||||
|
var newUser = userSignupDto.ToUser();
|
||||||
|
return await _userManager.CreateAsync(newUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
internal abstract class BaseCryptoItem
|
||||||
|
{
|
||||||
|
public enum HashAlgorithm
|
||||||
|
{
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
SHA2_512 = 1,
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
PBKDF2_SHA512 = 2,
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
SHA3_512 = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HexStringToByteArray
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringInHexFormat"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected byte[] HexStringToByteArray(string stringInHexFormat)
|
||||||
|
{
|
||||||
|
var converted = Enumerable.Range(0, stringInHexFormat.Length)
|
||||||
|
.Where(x => x % 2 == 0)
|
||||||
|
.Select(x => Convert.ToByte(stringInHexFormat.Substring(x, 2), 16))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ByteArrayToString
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bytes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string ByteArrayToString(byte[] bytes)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
sb.Append(bytes[i].ToString("X2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a random string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">The length of the random string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string CreateRandomString(int length)
|
||||||
|
{
|
||||||
|
var rng = RandomNumberGenerator.Create();
|
||||||
|
var buffer = new byte[length / 2];
|
||||||
|
rng.GetBytes(buffer);
|
||||||
|
var randomString = BitConverter.ToString(buffer).Replace("-", "");
|
||||||
|
return randomString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Refactor me
|
||||||
|
/// <summary>
|
||||||
|
/// Get Cryptographic algorithm
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cipherText"></param>
|
||||||
|
/// <param name="algorithm"></param>
|
||||||
|
/// <param name="keyIndex"></param>
|
||||||
|
/// <param name="trimmedCipherText"></param>
|
||||||
|
protected void GetAlgorithm(string cipherText, out int? algorithm, out int? keyIndex,
|
||||||
|
out string trimmedCipherText)
|
||||||
|
{
|
||||||
|
algorithm = null;
|
||||||
|
keyIndex = null;
|
||||||
|
trimmedCipherText = cipherText;
|
||||||
|
|
||||||
|
if (cipherText.Length <= 5 ) return;
|
||||||
|
|
||||||
|
var cipherInfo = cipherText[0].ToString();
|
||||||
|
|
||||||
|
if (int.TryParse(cipherInfo, out int foundAlgorithm))
|
||||||
|
{
|
||||||
|
algorithm = foundAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(cipherInfo, out int foundKeyIndex))
|
||||||
|
keyIndex = foundKeyIndex;
|
||||||
|
|
||||||
|
trimmedCipherText = cipherText.Substring(cipherText.IndexOf(cipherInfo, StringComparison.Ordinal) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hasher
|
||||||
|
/// </summary>
|
||||||
|
internal class Hasher : BaseCryptoItem, IHasher
|
||||||
|
{
|
||||||
|
private const int SaltLength = 64;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CreateHash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="algorithm"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string CreateHash(string text, HashAlgorithm algorithm)
|
||||||
|
{
|
||||||
|
var salt = CreateRandomString(SaltLength);
|
||||||
|
return CreateHash(text, salt, algorithm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CreateHash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="saltName"></param>
|
||||||
|
/// <param name="algorithm"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string CreateHash(string text, string saltName, BaseCryptoItem.HashAlgorithm algorithm)
|
||||||
|
{
|
||||||
|
return CreateHash(text, saltName, algorithm, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateHash(string text, string salt, HashAlgorithm algorithm, bool storeSalt)
|
||||||
|
{
|
||||||
|
string hash;
|
||||||
|
|
||||||
|
switch (algorithm)
|
||||||
|
{
|
||||||
|
case HashAlgorithm.SHA2_512:
|
||||||
|
var sha2 = new SHA2_512();
|
||||||
|
hash = sha2.Hash(text, salt, storeSalt);
|
||||||
|
break;
|
||||||
|
case HashAlgorithm.SHA3_512:
|
||||||
|
var sha3 = new SHA2_512();
|
||||||
|
hash = sha3.Hash(text, salt, storeSalt);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check for a matching hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="hash"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool MatchesHash(string text, string hash)
|
||||||
|
{
|
||||||
|
string salt = "";
|
||||||
|
|
||||||
|
GetAlgorithm(hash, out var algoAsInt, out _, out _, out salt);
|
||||||
|
|
||||||
|
if (!algoAsInt.HasValue) return false;
|
||||||
|
|
||||||
|
var hashAlgorithm = (HashAlgorithm) algoAsInt.Value;
|
||||||
|
var hashed = CreateHash(text, salt, hashAlgorithm, true);
|
||||||
|
return hashed == hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hash password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ApplicationUser"></param>
|
||||||
|
/// <param name="password"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string HashPassword(ApplicationUser ApplicationUser, string password)
|
||||||
|
{
|
||||||
|
return CreateHash(password, HashAlgorithm.SHA3_512);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordVerificationResult VerifyHashedPassword(ApplicationUser ApplicationUser, string hashedPassword,
|
||||||
|
string providedPassword)
|
||||||
|
{
|
||||||
|
var match = MatchesHash(providedPassword, hashedPassword);
|
||||||
|
return match ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetAlgorithm(string cipherText, out int? algorithm, out int? keyIndex,
|
||||||
|
out string trimmedCipherText, out string salt)
|
||||||
|
{
|
||||||
|
GetAlgorithm(cipherText, out algorithm, out keyIndex, out trimmedCipherText);
|
||||||
|
if (algorithm.HasValue && trimmedCipherText.Length > SaltLength)
|
||||||
|
{
|
||||||
|
salt = trimmedCipherText.Substring(0, SaltLength);
|
||||||
|
trimmedCipherText = trimmedCipherText.Substring(SaltLength);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
salt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.Cryptography;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IHasher contract
|
||||||
|
/// </summary>
|
||||||
|
internal interface IHasher : IPasswordHasher<ApplicationUser>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="algorithm"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string CreateHash(string text, BaseCryptoItem.HashAlgorithm algorithm);
|
||||||
|
/// <summary>
|
||||||
|
/// Create hash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="salt"></param>
|
||||||
|
/// <param name="algorithm"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string CreateHash(string text, string salt, BaseCryptoItem.HashAlgorithm algorithm);
|
||||||
|
/// <summary>
|
||||||
|
/// MatchesHash
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="hash"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool MatchesHash(string text, string hash);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public interface IJwtFactory
|
||||||
|
{
|
||||||
|
Task<AccessToken> GenerateEncodedToken(string id, string userName);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
public interface IJwtTokenHandler
|
||||||
|
{
|
||||||
|
string WriteToken(JwtSecurityToken jwt);
|
||||||
|
ClaimsPrincipal ValidateToken(string token, TokenValidationParameters tokenValidationParameters);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
public interface ISessionHasher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a token for the current session
|
||||||
|
/// </summary>
|
||||||
|
void GenerateSessionToken();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
internal interface ITokenFactory
|
||||||
|
{
|
||||||
|
string GenerateToken(int size= 32);
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using static BlueWest.WebApi.Context.Users.Constants;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
internal class JwtFactory : IJwtFactory
|
||||||
|
{
|
||||||
|
private readonly IJwtTokenHandler _jwtTokenHandler;
|
||||||
|
private readonly JwtIssuerOptions _jwtOptions;
|
||||||
|
|
||||||
|
public JwtFactory(IJwtTokenHandler jwtTokenHandler, IOptions<JwtIssuerOptions> jwtOptions)
|
||||||
|
{
|
||||||
|
_jwtTokenHandler = jwtTokenHandler;
|
||||||
|
_jwtOptions = jwtOptions.Value;
|
||||||
|
ThrowIfInvalidOptions(_jwtOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AccessToken> GenerateEncodedToken(string id, string userName)
|
||||||
|
{
|
||||||
|
var identity = GenerateClaimsIdentity(id, userName);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, userName),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Aud, _jwtOptions.Audience),
|
||||||
|
|
||||||
|
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(),
|
||||||
|
ClaimValueTypes.Integer64),
|
||||||
|
identity.FindFirst(JwtClaimIdentifiers.Rol),
|
||||||
|
identity.FindFirst(JwtClaimIdentifiers.Id)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the JWT security token and encode it.
|
||||||
|
var jwt = new JwtSecurityToken(
|
||||||
|
_jwtOptions.Issuer,
|
||||||
|
_jwtOptions.Audience,
|
||||||
|
claims,
|
||||||
|
_jwtOptions.NotBefore,
|
||||||
|
_jwtOptions.Expiration,
|
||||||
|
_jwtOptions.SigningCredentials);
|
||||||
|
|
||||||
|
return new AccessToken(_jwtTokenHandler.WriteToken(jwt), (int)_jwtOptions.ValidFor.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClaimsIdentity GenerateClaimsIdentity(string id, string userName)
|
||||||
|
{
|
||||||
|
return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtClaimIdentifiers.Id, id),
|
||||||
|
new Claim(JwtClaimIdentifiers.Rol, JwtClaims.ApiAccess)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC).</returns>
|
||||||
|
private static long ToUnixEpochDate(DateTime date)
|
||||||
|
=> (long)Math.Round((date.ToUniversalTime() -
|
||||||
|
new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero))
|
||||||
|
.TotalSeconds);
|
||||||
|
|
||||||
|
private static void ThrowIfInvalidOptions(JwtIssuerOptions options)
|
||||||
|
{
|
||||||
|
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
if (options.ValidFor <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.SigningCredentials == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.JtiGenerator == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
internal class JwtIssuerOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT.
|
||||||
|
/// </summary>
|
||||||
|
public string Issuer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT.
|
||||||
|
/// </summary>
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for.
|
||||||
|
/// </summary>
|
||||||
|
public string Audience { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Expiration => IssuedAt.Add(ValidFor);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime NotBefore => DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime IssuedAt => DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the timespan the token will be valid for (default is 120 min)
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ValidFor { get; set; } = SessionConstants.DefaultSessionMaxAge;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "jti" (JWT ID) Claim (default ID is a GUID)
|
||||||
|
/// </summary>
|
||||||
|
public Func<Task<string>> JtiGenerator =>
|
||||||
|
() => Task.FromResult(Guid.NewGuid().ToString());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The signing key to use when generating tokens.
|
||||||
|
/// </summary>
|
||||||
|
public SigningCredentials SigningCredentials { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class JwtTokenHandler : IJwtTokenHandler
|
||||||
|
{
|
||||||
|
private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JwtTokenHandler
|
||||||
|
/// </summary>
|
||||||
|
public JwtTokenHandler()
|
||||||
|
{
|
||||||
|
_jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write token
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jwt"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string WriteToken(JwtSecurityToken jwt)
|
||||||
|
{
|
||||||
|
return _jwtSecurityTokenHandler.WriteToken(jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate Token
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <param name="tokenValidationParameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="SecurityTokenException"></exception>
|
||||||
|
public ClaimsPrincipal ValidateToken(string token, TokenValidationParameters tokenValidationParameters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var principal = _jwtSecurityTokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
|
||||||
|
|
||||||
|
if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
throw new SecurityTokenException("Invalid token");
|
||||||
|
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SHA2_512 : BaseCryptoItem
|
||||||
|
/// </summary>
|
||||||
|
internal class SHA2_512 : BaseCryptoItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Hash with the provided salt
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="salt"></param>
|
||||||
|
/// <param name="storeSalt"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string Hash(string text, string salt, bool storeSalt)
|
||||||
|
{
|
||||||
|
var fullText = string.Concat(text, salt);
|
||||||
|
var data = Encoding.UTF8.GetBytes(fullText);
|
||||||
|
string hash;
|
||||||
|
using SHA512 sha = SHA512.Create();
|
||||||
|
var hashBytes = sha.ComputeHash(data);
|
||||||
|
var asString = ByteArrayToString(hashBytes);
|
||||||
|
|
||||||
|
if (storeSalt)
|
||||||
|
{
|
||||||
|
hash = $"{(int)HashAlgorithm.SHA3_512}{salt}{asString}";
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = $"{(int)HashAlgorithm.SHA3_512}{asString}";
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hash_PBKDF2 algorithm.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plainText"></param>
|
||||||
|
/// <param name="salt"></param>
|
||||||
|
/// <param name="saveSaltInResult"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string Hash_PBKDF2(string plainText, string salt, bool saveSaltInResult)
|
||||||
|
{
|
||||||
|
var saltAsBytes = Encoding.ASCII.GetBytes(salt);
|
||||||
|
|
||||||
|
string hashed = ByteArrayToString(KeyDerivation.Pbkdf2(
|
||||||
|
password: plainText,
|
||||||
|
salt: saltAsBytes,
|
||||||
|
prf: KeyDerivationPrf.HMACSHA512, //.NET 3.1 uses HMACSHA256 here
|
||||||
|
iterationCount: 100000, //.NET 3.1 uses 10,000 iterations here
|
||||||
|
numBytesRequested: 64)); //.NET 3.1 uses 32 bytes here
|
||||||
|
|
||||||
|
if (saveSaltInResult)
|
||||||
|
return string.Format("[{0}]{1}{2}", (int)HashAlgorithm.PBKDF2_SHA512, salt, hashed);
|
||||||
|
else
|
||||||
|
return string.Format("[{0}]{1}", (int)HashAlgorithm.PBKDF2_SHA512, hashed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data.Application;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auth manager contract interface.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CreateUserAsync
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registerViewModel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IdentityResult> CreateUserAsync(RegisterViewModel registerViewModel);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does Login
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loginRequest"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequest(LoginRequest loginRequest);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SignInManager
|
||||||
|
/// </summary>
|
||||||
|
internal class SignInManager : SignInManager<ApplicationUser>
|
||||||
|
{
|
||||||
|
public SignInManager(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IHttpContextAccessor contextAccessor,
|
||||||
|
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
|
||||||
|
IOptions<IdentityOptions> optionsAccessor,
|
||||||
|
ILogger<SignInManager<ApplicationUser>> logger,
|
||||||
|
IAuthenticationSchemeProvider schemes,
|
||||||
|
IUserConfirmation<ApplicationUser> confirmation) :
|
||||||
|
base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JwtClaimIdentifiers
|
||||||
|
/// </summary>
|
||||||
|
public static class JwtClaimIdentifiers
|
||||||
|
{
|
||||||
|
public const string Rol = "rol", Id = "id";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JwtClaims
|
||||||
|
/// </summary>
|
||||||
|
public static class JwtClaims
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JwtClaims.ApiAccess
|
||||||
|
/// </summary>
|
||||||
|
public const string ApiAccess = "api_access";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data.Application;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
public interface IUserManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IdentityResult> CreateAsync(ApplicationUser user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for user password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="password"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> CheckPasswordAsync(ApplicationUser user, string password);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find by email
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ApplicationUser> FindByEmailAsync(string email);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Domain;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Users Repository
|
||||||
|
/// </summary>
|
||||||
|
public class UserRepository : UserStore<ApplicationUser,
|
||||||
|
ApplicationRole,
|
||||||
|
ApplicationUserDbContext,
|
||||||
|
string,
|
||||||
|
ApplicationUserClaim,
|
||||||
|
ApplicationUserRole,
|
||||||
|
ApplicationUserLogin,
|
||||||
|
ApplicationUserToken,
|
||||||
|
ApplicationRoleClaim>
|
||||||
|
{
|
||||||
|
private readonly ApplicationUserDbContext _context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User repository
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <param name="describer"></param>
|
||||||
|
public UserRepository(ApplicationUserDbContext context, IdentityErrorDescriber describer = null) : base(context, describer)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get Application Users
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IEnumerable<ApplicationUser>> GetUsers()
|
||||||
|
{
|
||||||
|
var users = await _context.Users.ToListAsync();
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.PasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<bool>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Domain", "BlueWest
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Razor.Library", "BlueWest.Razor.Library\BlueWest.Razor.Library.csproj", "{CA6DF60F-B33E-4688-A4ED-4427B446E852}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Razor.Library", "BlueWest.Razor.Library\BlueWest.Razor.Library.csproj", "{CA6DF60F-B33E-4688-A4ED-4427B446E852}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Data.Auth", "BlueWest.Data.Auth\BlueWest.Data.Auth.csproj", "{2998FE17-18AD-4888-A696-7F6340F8A543}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -97,6 +99,10 @@ Global
|
||||||
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2998FE17-18AD-4888-A696-7F6340F8A543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2998FE17-18AD-4888-A696-7F6340F8A543}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2998FE17-18AD-4888-A696-7F6340F8A543}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2998FE17-18AD-4888-A696-7F6340F8A543}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
Loading…
Reference in New Issue