Add auth module

This commit is contained in:
Wvader 2022-09-26 02:40:18 +01:00
parent 3e1555d052
commit 4e4a720aa4
26 changed files with 1217 additions and 0 deletions

View File

@ -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>

View File

@ -0,0 +1,5 @@
namespace BlueWest.Data.Auth;
public class Class1
{
}

View File

@ -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();
}
}

View File

@ -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}";
}
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}

View File

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace BlueWest.WebApi.Context.Users;
public interface IJwtFactory
{
Task<AccessToken> GenerateEncodedToken(string id, string userName);
}

View File

@ -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);
}
}

View File

@ -0,0 +1,12 @@
namespace BlueWest.Cryptography
{
public interface ISessionHasher
{
/// <summary>
/// Generates a token for the current session
/// </summary>
void GenerateSessionToken();
}
}

View File

@ -0,0 +1,6 @@
namespace BlueWest.WebApi.Context.Users;
internal interface ITokenFactory
{
string GenerateToken(int size= 32);
}

View File

@ -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));
}
}
}

View File

@ -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; }
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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)
{
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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