Add project files.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
namespace MyOffice.Web.Identity.Configure;
|
||||
|
||||
using MyOffice.Web.Infrastructure;
|
||||
using IdentityServer4.AccessTokenValidation;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class ConfigureIdentityServerOptions : IConfigureNamedOptions<IdentityServerAuthenticationOptions>
|
||||
{
|
||||
readonly GlobalSettings _globalSettings;
|
||||
readonly ILogger<ConfigureIdentityServerOptions> _logger;
|
||||
|
||||
public ConfigureIdentityServerOptions(
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<ConfigureIdentityServerOptions> logger
|
||||
)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Configure(string? name, IdentityServerAuthenticationOptions options)
|
||||
{
|
||||
//_logger.LogError($"Configure: {_globalSettings.Host}");
|
||||
if (name == IdentityServerAuthenticationDefaults.AuthenticationScheme)
|
||||
{
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.ApiName = IdentityServerConfig.ApiName;
|
||||
options.Authority = _globalSettings.Host;
|
||||
}
|
||||
}
|
||||
|
||||
// This won't be called, but is required for the IConfigureNamedOptions interface
|
||||
public void Configure(IdentityServerAuthenticationOptions options) => Configure(Options.DefaultName, options);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
namespace MyOffice.Web.Identity.Configure;
|
||||
|
||||
using IdentityModel;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
public class IdentityServerConfig
|
||||
{
|
||||
public static class ClaimConstants
|
||||
{
|
||||
public const string Subject = "subject";
|
||||
public const string Permission = "permission";
|
||||
}
|
||||
|
||||
public static class ScopeConstants
|
||||
{
|
||||
public const string Roles = "roles";
|
||||
}
|
||||
|
||||
public const string ApiName = "api";
|
||||
public const string ApiFriendlyName = "MyOffice.Web API";
|
||||
public const string AppClientID = "angulartemplate_spa";
|
||||
|
||||
public static IEnumerable<IdentityResource> GetIdentityResources()
|
||||
{
|
||||
return new List<IdentityResource>
|
||||
{
|
||||
new IdentityResources.OpenId(),
|
||||
new IdentityResources.Profile(),
|
||||
new IdentityResources.Phone(),
|
||||
new IdentityResources.Email(),
|
||||
new(ScopeConstants.Roles, new List<string> { JwtClaimTypes.Role })
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<ApiScope> GetApiScopes()
|
||||
{
|
||||
return new List<ApiScope>
|
||||
{
|
||||
new(ApiName, ApiFriendlyName)
|
||||
{
|
||||
UserClaims =
|
||||
{
|
||||
JwtClaimTypes.Name,
|
||||
JwtClaimTypes.Email,
|
||||
JwtClaimTypes.PhoneNumber,
|
||||
JwtClaimTypes.Role,
|
||||
ClaimConstants.Permission
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<ApiResource> GetApiResources()
|
||||
{
|
||||
return new List<ApiResource>
|
||||
{
|
||||
new(ApiName)
|
||||
{
|
||||
Scopes = { ApiName }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<Client> GetClients()
|
||||
{
|
||||
return new List<Client>
|
||||
{
|
||||
new()
|
||||
{
|
||||
ClientId = AppClientID,
|
||||
ClientSecrets = new List<Secret>
|
||||
{
|
||||
new Secret("secret".Sha256())
|
||||
},
|
||||
AllowedGrantTypes = new[]
|
||||
{
|
||||
GrantType.AuthorizationCode,
|
||||
GrantType.ClientCredentials,
|
||||
GrantType.DeviceFlow,
|
||||
GrantType.ResourceOwnerPassword,
|
||||
"external"
|
||||
},
|
||||
AllowAccessTokensViaBrowser = true,
|
||||
RequireClientSecret = false,
|
||||
|
||||
RedirectUris = new[]
|
||||
{
|
||||
"http://localhost:4200/silent-refresh.html",
|
||||
},
|
||||
|
||||
AllowedScopes =
|
||||
{
|
||||
IdentityServerConstants.StandardScopes.OpenId,
|
||||
IdentityServerConstants.StandardScopes.Profile,
|
||||
IdentityServerConstants.StandardScopes.Phone,
|
||||
IdentityServerConstants.StandardScopes.Email,
|
||||
ScopeConstants.Roles,
|
||||
ApiName
|
||||
},
|
||||
AllowOfflineAccess = true,
|
||||
RefreshTokenExpiration = TokenExpiration.Sliding,
|
||||
RefreshTokenUsage = TokenUsage.OneTimeOnly
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace MyOffice.Web.Identity;
|
||||
|
||||
using Core.Extensions;
|
||||
using IdentityServer4.Services;
|
||||
using Infrastructure;
|
||||
|
||||
public class CorsPolicyService : ICorsPolicyService
|
||||
{
|
||||
private readonly ILogger<CorsPolicyService> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public CorsPolicyService(
|
||||
ILogger<CorsPolicyService> logger,
|
||||
GlobalSettings globalSettings
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public Task<bool> IsOriginAllowedAsync(string origin)
|
||||
{
|
||||
|
||||
var result = origin.EqualsIgnoreCase(_globalSettings.Host);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace MyOffice.Web.Identity.Domain;
|
||||
|
||||
public class ApplicationRole
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace MyOffice.Web.Identity.Domain;
|
||||
|
||||
public class ApplicationUser<TKey>
|
||||
{
|
||||
#pragma warning disable CS8618 // Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
|
||||
public TKey Id { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable property 'Id' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
|
||||
public string UserName { get; set; } = null!;
|
||||
public string Email { get; set; } = null!;
|
||||
public string PasswordHash { get; set; } = null!;
|
||||
|
||||
public string? FirstName { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public string? FullName { get; set; }
|
||||
public bool IsEmailConfirmed { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace MyOffice.Web.Identity.Domain;
|
||||
|
||||
public class ExternalProvidersConfig
|
||||
{
|
||||
public class Auth0Config
|
||||
{
|
||||
public string? ClientId { get; set; }
|
||||
public string? Domain { get; set; }
|
||||
public string? SecretKey { get; set; }
|
||||
}
|
||||
|
||||
public class GoogleConfig
|
||||
{
|
||||
public string? ClientId { get; set; }
|
||||
}
|
||||
|
||||
public Auth0Config? Auth0 { get; set; }
|
||||
public GoogleConfig? Google { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
namespace MyOffice.Web.Identity;
|
||||
|
||||
using MyOffice.Data.Models.Users;
|
||||
using Domain;
|
||||
using Data.Repositories.Users;
|
||||
using Repositories;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
using Core.Extensions;
|
||||
using Core.Identity;
|
||||
|
||||
public class ExtensionGrantValidator : IExtensionGrantValidator
|
||||
{
|
||||
private readonly IUserStore<ApplicationUser<Guid>> _userStore;
|
||||
private readonly ILogger<ExtensionGrantValidator> _logger;
|
||||
private readonly AppUserManager _appUserManager;
|
||||
private readonly IUserExternalRepository _userExternalRepository;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ExternalProvidersConfig _externalProvidersConfig;
|
||||
private readonly IEnumerable<IExternalProviderValidator> _externalProviderValidators;
|
||||
|
||||
public ExtensionGrantValidator(
|
||||
ILogger<ExtensionGrantValidator> logger,
|
||||
IUserStore<ApplicationUser<Guid>> userStore,
|
||||
AppUserManager appUserManager,
|
||||
IUserExternalRepository userExternalRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptions<ExternalProvidersConfig> externalProvidersConfig,
|
||||
IEnumerable<IExternalProviderValidator> externalProviderValidators
|
||||
)
|
||||
{
|
||||
if (externalProvidersConfig == null)
|
||||
throw new ArgumentNullException(nameof(externalProvidersConfig));
|
||||
|
||||
_userStore = userStore ?? throw new ArgumentNullException(nameof(userStore));
|
||||
_appUserManager = appUserManager ?? throw new ArgumentNullException(nameof(appUserManager));
|
||||
_userExternalRepository =
|
||||
userExternalRepository ?? throw new ArgumentNullException(nameof(userExternalRepository));
|
||||
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||
_externalProvidersConfig = externalProvidersConfig.Value;
|
||||
_externalProviderValidators = externalProviderValidators;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public string GrantType => "external";
|
||||
|
||||
public async Task ValidateAsync(ExtensionGrantValidationContext context)
|
||||
{
|
||||
var provider = context.Request.Raw["provider"];
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException("provider not found");
|
||||
|
||||
var validator = _externalProviderValidators.FirstOrDefault(x => x.Provider.EqualsIgnoreCase(provider));
|
||||
if (validator == null || !validator.IsConfigured)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest,
|
||||
$"Provider not supported: {provider}");
|
||||
return;
|
||||
}
|
||||
|
||||
var token = context.Request.Raw["token"];
|
||||
if (token == null)
|
||||
throw new ArgumentNullException("token not found");
|
||||
|
||||
var result = await validator.ValidateAsync(token);
|
||||
if (!result.IsSuccessed)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, $"Token not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
context.Result = await ProcessAsync(result.Email!, result.ExternalId!, provider, result.EmailVerified,
|
||||
result.FullName);
|
||||
}
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
private async Task<GrantValidationResult> ProcessAsync(
|
||||
string email,
|
||||
string externlaId,
|
||||
string provider,
|
||||
bool isEmailConfirmed,
|
||||
string? fullName
|
||||
)
|
||||
{
|
||||
// connect to exists user
|
||||
if (_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return new GrantValidationResult(TokenRequestErrors.InvalidRequest, $"Authentication failed");
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await _appUserManager.FindByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
if (!isEmailConfirmed)
|
||||
return new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Email not confirmed");
|
||||
|
||||
user = await CreateUser(email, isEmailConfirmed, fullName);
|
||||
if (user == null) return new GrantValidationResult(TokenRequestErrors.InvalidRequest);
|
||||
}
|
||||
|
||||
var claim = await _userExternalRepository.GetByUserIdAsync(user.Id, provider);
|
||||
if (claim == null) claim = AddClaim(user, email, externlaId, provider);
|
||||
|
||||
return ClaimToGrantValidationResult(claim.UserId, "", "");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ApplicationUser<Guid>?> CreateUser(string email, bool isEmailConfirmed, string? fullName)
|
||||
{
|
||||
var user = new ApplicationUser<Guid>
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserName = email,
|
||||
Email = email,
|
||||
IsEmailConfirmed = isEmailConfirmed,
|
||||
FullName = fullName
|
||||
};
|
||||
var password = "Qq1!_" + Guid.NewGuid();
|
||||
var result = await _appUserManager.CreateAsync(user, password);
|
||||
if (!result.Succeeded) return null;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private UserExternal AddClaim(ApplicationUser<Guid> user, string email, string externalId, string provider)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var claim = _userExternalRepository.GetByUserId(user.Id, provider);
|
||||
|
||||
if (claim != null) return claim;
|
||||
|
||||
claim = new UserExternal
|
||||
{
|
||||
UserId = user.Id,
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Provider = provider.ToLower(),
|
||||
ExternalId = externalId,
|
||||
Email = email
|
||||
};
|
||||
_userExternalRepository.AddUserExternal(claim);
|
||||
|
||||
return claim;
|
||||
}
|
||||
}
|
||||
|
||||
private GrantValidationResult ClaimToGrantValidationResult(Guid userId, string provider, string email)
|
||||
{
|
||||
var userIdStr = userId.ToString();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtClaimTypes.Id, userIdStr),
|
||||
new(JwtClaimTypes.Subject, userIdStr),
|
||||
new(JwtClaimTypes.Email, email)
|
||||
};
|
||||
|
||||
var grantValidationResult = new GrantValidationResult(userIdStr, provider, claims);
|
||||
|
||||
return grantValidationResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace MyOffice.Web.Identity.ExternalProviders;
|
||||
|
||||
using Domain;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text;
|
||||
using Core.Extensions;
|
||||
using Core.Identity;
|
||||
|
||||
public class ExternalProviderValidatorAuth0 : IExternalProviderValidator
|
||||
{
|
||||
private readonly ExternalProvidersConfig _externalProvidersConfig;
|
||||
private readonly ILogger<ExternalProviderValidatorAuth0> _logger;
|
||||
|
||||
public ExternalProviderValidatorAuth0(
|
||||
ILogger<ExternalProviderValidatorAuth0> logger,
|
||||
IOptions<ExternalProvidersConfig> externalProvidersConfig
|
||||
)
|
||||
{
|
||||
_externalProvidersConfig = externalProvidersConfig.Value;
|
||||
_logger = logger;
|
||||
|
||||
Provider = ExternalProvidersConst.PROVIDER_AUTH0;
|
||||
IsConfigured = _externalProvidersConfig.IsAuth0Configured();
|
||||
}
|
||||
|
||||
public string Provider { get; }
|
||||
public bool IsConfigured { get; }
|
||||
|
||||
public async Task<ExternalProviderValidatorResult> ValidateAsync(string token)
|
||||
{
|
||||
var auth0Domain = _externalProvidersConfig.Auth0!.Domain!;
|
||||
var secretKey = _externalProvidersConfig.Auth0!.SecretKey!;
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
|
||||
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
|
||||
$"{auth0Domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
|
||||
var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
|
||||
|
||||
var validations = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = auth0Domain,
|
||||
ValidAudiences = new[] { _externalProvidersConfig.Auth0!.ClientId },
|
||||
IssuerSigningKeys = openIdConfig.SigningKeys,
|
||||
TokenDecryptionKey = securityKey
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var user = tokenHandler.ValidateToken(token, validations, out var validatedToken);
|
||||
if (user.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return ExternalProviderValidatorResult.Failed();
|
||||
}
|
||||
|
||||
var email = user.Claims.GetEmail();
|
||||
var emailVerified = user.Claims.GetValue("email_verified").AsBool();
|
||||
var fullName = user.Claims.GetValue("name")?.ToString();
|
||||
var externalId = user.Claims.GetSID();
|
||||
|
||||
if (email == null || externalId == null)
|
||||
{
|
||||
return ExternalProviderValidatorResult.Failed();
|
||||
}
|
||||
|
||||
return ExternalProviderValidatorResult.Success(email, emailVerified, externalId, fullName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace MyOffice.Web.Identity.ExternalProviders;
|
||||
|
||||
using MyOffice.Core.Identity;
|
||||
using Domain;
|
||||
using Google.Apis.Auth;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class ExternalProviderValidatorGoogle : IExternalProviderValidator
|
||||
{
|
||||
private readonly ExternalProvidersConfig _externalProvidersConfig;
|
||||
|
||||
public ExternalProviderValidatorGoogle(
|
||||
IOptions<ExternalProvidersConfig> externalProvidersConfig
|
||||
)
|
||||
{
|
||||
_externalProvidersConfig = externalProvidersConfig.Value;
|
||||
|
||||
Provider = ExternalProvidersConst.PROVIDER_GOOGLE;
|
||||
IsConfigured = _externalProvidersConfig.IsGoogleConfigured();
|
||||
}
|
||||
|
||||
public string Provider { get; }
|
||||
public bool IsConfigured { get; }
|
||||
|
||||
public async Task<ExternalProviderValidatorResult> ValidateAsync(string token)
|
||||
{
|
||||
var settings = new GoogleJsonWebSignature.ValidationSettings()
|
||||
{
|
||||
Audience = new List<string>() { _externalProvidersConfig.Google!.ClientId! }
|
||||
};
|
||||
var result = await GoogleJsonWebSignature.ValidateAsync(token, settings);
|
||||
if (!result.EmailVerified) return ExternalProviderValidatorResult.Failed();
|
||||
|
||||
return ExternalProviderValidatorResult.Success(result.Email, result.EmailVerified, result.Subject, result.Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MyOffice.Web.Identity.ExternalProviders;
|
||||
|
||||
public class ExternalProvidersConst
|
||||
{
|
||||
public const string PROVIDER_GOOGLE = "google";
|
||||
public const string PROVIDER_AUTH0 = "auth0";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using MyOffice.Web.Identity.Domain;
|
||||
|
||||
namespace MyOffice.Web.Identity;
|
||||
|
||||
using Core.Extensions;
|
||||
|
||||
public static class ExternalProvidersConfigExtensions
|
||||
{
|
||||
public static bool IsGoogleConfigured(this ExternalProvidersConfig? config)
|
||||
{
|
||||
return config != null
|
||||
&& config.Google != null
|
||||
&& config.Google.ClientId.IsPresent();
|
||||
}
|
||||
|
||||
public static bool IsAuth0Configured(this ExternalProvidersConfig? config)
|
||||
{
|
||||
return config != null
|
||||
&& config.Auth0 != null
|
||||
&& config.Auth0.ClientId.IsPresent()
|
||||
&& config.Auth0.Domain.IsPresent()
|
||||
&& config.Auth0.SecretKey.IsPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace MyOffice.Web.Identity;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using Core.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
|
||||
public class PersistedGrantStore : IPersistedGrantStore
|
||||
{
|
||||
private static ConcurrentDictionary<string, PersistedGrant> _store = new ConcurrentDictionary<string, PersistedGrant>();
|
||||
|
||||
public Task StoreAsync(PersistedGrant grant)
|
||||
{
|
||||
_store.AddOrUpdate(grant.Key, grant, (key, oldValue) => grant);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<PersistedGrant> GetAsync(string key)
|
||||
{
|
||||
if (!_store.TryGetValue(key, out var grant))
|
||||
throw new Exception($"Key '{key}' not found");
|
||||
|
||||
return Task.FromResult(grant);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
|
||||
{
|
||||
var list = _store.Values.Where(x =>
|
||||
(filter.ClientId.IsMissing() || filter.ClientId == x.ClientId)
|
||||
&& (filter.SessionId.IsMissing() || filter.SessionId == x.SessionId)
|
||||
&& (filter.SubjectId.IsMissing() || filter.SubjectId == x.SubjectId)
|
||||
&& (filter.Type.IsMissing() || filter.Type == x.Type)
|
||||
);
|
||||
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
public Task RemoveAsync(string key)
|
||||
{
|
||||
_store.TryRemove(key, out _);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task RemoveAllAsync(PersistedGrantFilter filter)
|
||||
{
|
||||
var list = await GetAllAsync(filter);
|
||||
foreach (var item in list)
|
||||
{
|
||||
await RemoveAsync(item.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace MyOffice.Web.Identity;
|
||||
|
||||
using Data.Repositories.Users;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using System.Security.Claims;
|
||||
|
||||
public class ProfileService : IProfileService
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ProfileService(
|
||||
IUserRepository userRepository
|
||||
)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
{
|
||||
var sub = context.Subject.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
|
||||
if (sub != null && Guid.TryParse(sub.Value, out var id))
|
||||
{
|
||||
var user = await _userRepository.GetUserAsync(id);
|
||||
if (user != null)
|
||||
if (context.RequestedClaimTypes.Contains(JwtClaimTypes.Email))
|
||||
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Email, user.Email));
|
||||
}
|
||||
}
|
||||
|
||||
public Task IsActiveAsync(IsActiveContext context)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
namespace MyOffice.Web.Identity.Repositories;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using Domain;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class PasswordHasher : IPasswordHasher<ApplicationUser<Guid>>
|
||||
{
|
||||
public string HashPassword(ApplicationUser<Guid> user, string password)
|
||||
{
|
||||
byte[] salt;
|
||||
salt = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
|
||||
var hash = pbkdf2.GetBytes(20);
|
||||
|
||||
var hashBytes = new byte[36];
|
||||
Array.Copy(salt, 0, hashBytes, 0, 16);
|
||||
Array.Copy(hash, 0, hashBytes, 16, 20);
|
||||
|
||||
var passwordHash = Convert.ToBase64String(hashBytes);
|
||||
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
private bool IsValidHash(string password, string passwordHash)
|
||||
{
|
||||
/* Fetch the stored value */
|
||||
/* Extract the bytes */
|
||||
var hashBytes = Convert.FromBase64String(passwordHash);
|
||||
/* Get the salt */
|
||||
var salt = new byte[16];
|
||||
Array.Copy(hashBytes, 0, salt, 0, 16);
|
||||
/* Compute the hash on the password the user entered */
|
||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100000);
|
||||
var hash = pbkdf2.GetBytes(20);
|
||||
/* Compare the results */
|
||||
for (var i = 0; i < 20; i++)
|
||||
if (hashBytes[i + 16] != hash[i])
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(
|
||||
ApplicationUser<Guid> user,
|
||||
string hashedPassword,
|
||||
string providedPassword
|
||||
)
|
||||
{
|
||||
if (IsValidHash(providedPassword, hashedPassword)) return PasswordVerificationResult.Success;
|
||||
|
||||
return PasswordVerificationResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
public class AppUserManager : UserManager<ApplicationUser<Guid>>
|
||||
{
|
||||
public AppUserManager(
|
||||
IUserStore<ApplicationUser<Guid>> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<ApplicationUser<Guid>> passwordHasher,
|
||||
IEnumerable<IUserValidator<ApplicationUser<Guid>>> userValidators,
|
||||
IEnumerable<IPasswordValidator<ApplicationUser<Guid>>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<ApplicationUser<Guid>>> logger) :
|
||||
base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
passwordHasher,
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
keyNormalizer,
|
||||
errors,
|
||||
services,
|
||||
logger
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace MyOffice.Web.Identity.Repositories;
|
||||
|
||||
using Domain;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
public class RoleStore : IRoleStore<ApplicationRole>
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public Task<IdentityResult> CreateAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> DeleteAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetRoleIdAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetRoleNameAsync(ApplicationRole role, string roleName, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetNormalizedRoleNameAsync(ApplicationRole role, string normalizedName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ApplicationRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ApplicationRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
namespace MyOffice.Web.Identity.Repositories;
|
||||
|
||||
using Data.Models.Users;
|
||||
using Data.Repositories.Users;
|
||||
using Domain;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
public class UserStore :
|
||||
IUserStore<ApplicationUser<Guid>>,
|
||||
IUserPasswordStore<ApplicationUser<Guid>>,
|
||||
IUserEmailStore<ApplicationUser<Guid>>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public UserStore(
|
||||
IUserRepository userRepository
|
||||
)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public Task<string> GetUserIdAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.Id.ToString());
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.UserName);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(ApplicationUser<Guid> user, string userName, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedUserNameAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(ApplicationUser<Guid> user, string normalizedName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
user.UserName = normalizedName;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> CreateAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _userRepository.AddUserAsync(new User
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
PasswordHash = user.PasswordHash,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
FullName = user.FullName,
|
||||
IsEmailConfirmed = user.IsEmailConfirmed
|
||||
});
|
||||
|
||||
return result == 1 ? IdentityResult.Success : IdentityResult.Failed();
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> UpdateAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
var userDb = new User
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
PasswordHash = user.PasswordHash,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
FullName = user.FullName,
|
||||
IsEmailConfirmed = user.IsEmailConfirmed
|
||||
};
|
||||
|
||||
var result = await _userRepository.UpdateUserAsync(userDb);
|
||||
return result == 1 ? IdentityResult.Success : IdentityResult.Failed();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> DeleteAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<ApplicationUser<Guid>> FindByIdAsync(string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetUserAsync(Guid.Parse(userId));
|
||||
if (user == null)
|
||||
{
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
return null;
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
}
|
||||
|
||||
return new ApplicationUser<Guid>
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
PasswordHash = user.PasswordHash,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
FullName = user.FullName,
|
||||
IsEmailConfirmed = user.IsEmailConfirmed
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ApplicationUser<Guid>> FindByNameAsync(string normalizedUserName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userRepository.GetByUserUserNameAsync(normalizedUserName);
|
||||
if (user == null)
|
||||
{
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
return null;
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
}
|
||||
|
||||
return new ApplicationUser<Guid>
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
PasswordHash = user.PasswordHash,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
FullName = user.FullName,
|
||||
IsEmailConfirmed = user.IsEmailConfirmed
|
||||
};
|
||||
}
|
||||
|
||||
public Task SetPasswordHashAsync(ApplicationUser<Guid> user, string passwordHash,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
user.PasswordHash = passwordHash;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<string> GetPasswordHashAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.PasswordHash);
|
||||
}
|
||||
|
||||
public Task<bool> HasPasswordAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetEmailAsync(ApplicationUser<Guid> user, string email, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetEmailAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<bool> GetEmailConfirmedAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(ApplicationUser<Guid> user, bool confirmed, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ApplicationUser<Guid>> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
|
||||
{
|
||||
return FindByNameAsync(normalizedEmail, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string?> GetNormalizedEmailAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user?.Email);
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(ApplicationUser<Guid> user, string normalizedEmail,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user