sync public allowlist from private myoffice
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
namespace MyOffice.Web.Identity;
|
||||
|
||||
using Data.Repositories.Users;
|
||||
using MyOffice.Data.Models.Users;
|
||||
using MyOffice.Services.Identity;
|
||||
|
||||
public class ContextProvider : IContextProvider
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ContextProvider(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IUserRepository userRepository
|
||||
)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
private User? _user = null;
|
||||
public User User
|
||||
{
|
||||
get
|
||||
{
|
||||
_user ??= _userRepository.GetUser(UserId);
|
||||
|
||||
return _user!;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
return Guid.Parse(_httpContextAccessor!.HttpContext!.User!.Claims!.FirstOrDefault(x => x.Type == "sub")!.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace MyOffice.Web.Identity.Domain;
|
||||
|
||||
public class ApplicationRole
|
||||
{
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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; }
|
||||
public string? CurrencyId { get; set; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace MyOffice.Web.Identity.ExternalProviders;
|
||||
|
||||
public class ExternalProvidersConst
|
||||
{
|
||||
public const string PROVIDER_GOOGLE = "google";
|
||||
public const string PROVIDER_AUTH0 = "auth0";
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
namespace MyOffice.Web.Auth;
|
||||
|
||||
using Core.Extensions;
|
||||
using Core.Identity;
|
||||
using Data.Models.Users;
|
||||
using Data.Repositories.Users;
|
||||
using Identity.Domain;
|
||||
using Identity.ExternalProviders;
|
||||
using Identity.Repositories;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
using static OpenIddict.Server.OpenIddictServerEvents;
|
||||
|
||||
public sealed class ExternalGrantHandler : IOpenIddictServerHandler<HandleTokenRequestContext>
|
||||
{
|
||||
private readonly AppUserManager _userManager;
|
||||
private readonly IUserExternalRepository _userExternalRepository;
|
||||
private readonly IEnumerable<IExternalProviderValidator> _externalProviderValidators;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ExternalGrantHandler(
|
||||
AppUserManager userManager,
|
||||
IUserExternalRepository userExternalRepository,
|
||||
IEnumerable<IExternalProviderValidator> externalProviderValidators,
|
||||
IHttpContextAccessor httpContextAccessor
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userExternalRepository = userExternalRepository;
|
||||
_externalProviderValidators = externalProviderValidators;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(HandleTokenRequestContext context)
|
||||
{
|
||||
if (!string.Equals(context.Request.GrantType, OpenIddictAuthConstants.ExternalGrantType, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var provider = context.Request.GetParameter("provider")?.ToString();
|
||||
if (provider.IsMissing())
|
||||
{
|
||||
context.Reject(Errors.InvalidRequest, "The provider parameter is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
var validator = _externalProviderValidators.FirstOrDefault(x => x.Provider.EqualsIgnoreCase(provider));
|
||||
if (validator is null || !validator.IsConfigured)
|
||||
{
|
||||
context.Reject(Errors.InvalidRequest, $"Provider not supported: {provider}");
|
||||
return;
|
||||
}
|
||||
|
||||
var token = context.Request.GetParameter("token")?.ToString();
|
||||
if (token.IsMissing())
|
||||
{
|
||||
context.Reject(Errors.InvalidRequest, "The token parameter is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
var validationResult = await validator.ValidateAsync(token);
|
||||
if (!validationResult.IsSuccessed)
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Token not valid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Authentication failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(validationResult.Email!);
|
||||
if (user is null)
|
||||
{
|
||||
if (!validationResult.EmailVerified)
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Email not confirmed.");
|
||||
return;
|
||||
}
|
||||
|
||||
user = await CreateUserAsync(validationResult.Email!, validationResult.EmailVerified, validationResult.FullName);
|
||||
if (user is null)
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Unable to create user.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var externalLogin = await _userExternalRepository.GetByUserIdAsync(user.Id, provider)
|
||||
?? AddExternalLogin(user, validationResult.Email!, validationResult.ExternalId!, provider);
|
||||
|
||||
context.SignIn(OpenIddictClaimsHelper.CreatePrincipal(user, context.Request.GetScopes()));
|
||||
}
|
||||
|
||||
private async Task<ApplicationUser<Guid>?> CreateUserAsync(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 _userManager.CreateAsync(user, password);
|
||||
return result.Succeeded ? user : null;
|
||||
}
|
||||
|
||||
private UserExternal AddExternalLogin(
|
||||
ApplicationUser<Guid> user,
|
||||
string email,
|
||||
string externalId,
|
||||
string provider
|
||||
)
|
||||
{
|
||||
var existing = _userExternalRepository.GetByUserId(user.Id, provider);
|
||||
if (existing is not null)
|
||||
return existing;
|
||||
|
||||
var claim = new UserExternal
|
||||
{
|
||||
UserId = user.Id,
|
||||
CreatedOn = DateTime.UtcNow,
|
||||
Provider = provider.ToLower(),
|
||||
ExternalId = externalId,
|
||||
Email = email
|
||||
};
|
||||
_userExternalRepository.AddUserExternal(claim);
|
||||
|
||||
return claim;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
namespace MyOffice.Web.Auth;
|
||||
|
||||
using System.Security.Claims;
|
||||
using Identity.Domain;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using OpenIddict.Abstractions;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
public static class OpenIddictClaimsHelper
|
||||
{
|
||||
public static ClaimsPrincipal CreatePrincipal(
|
||||
ApplicationUser<Guid> user,
|
||||
IEnumerable<string> scopes
|
||||
)
|
||||
{
|
||||
var identity = new ClaimsIdentity(
|
||||
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
|
||||
nameType: Claims.Name,
|
||||
roleType: Claims.Role);
|
||||
|
||||
var userId = user.Id.ToString();
|
||||
identity.SetClaim(Claims.Subject, userId);
|
||||
identity.SetClaim(Claims.Name, user.UserName);
|
||||
identity.SetClaim(Claims.PreferredUsername, user.UserName);
|
||||
identity.SetClaim(Claims.Email, user.Email);
|
||||
identity.SetClaim(Claims.EmailVerified, user.IsEmailConfirmed);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.FullName))
|
||||
identity.SetClaim(Claims.GivenName, user.FullName);
|
||||
|
||||
identity.SetScopes(scopes);
|
||||
identity.SetDestinations(static claim => claim.Type switch
|
||||
{
|
||||
Claims.Name or Claims.PreferredUsername or Claims.Email or Claims.EmailVerified or Claims.GivenName
|
||||
=> [Destinations.AccessToken, Destinations.IdentityToken],
|
||||
_ => [Destinations.AccessToken]
|
||||
});
|
||||
|
||||
return new ClaimsPrincipal(identity);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace MyOffice.Web.Auth;
|
||||
|
||||
public static class OpenIddictAuthConstants
|
||||
{
|
||||
public const string ApiScope = "api";
|
||||
public const string ApiFriendlyName = "MyOffice.Web API";
|
||||
public const string SpaClientId = "angulartemplate_spa";
|
||||
public const string ExternalGrantType = "external";
|
||||
public const string RolesScope = "roles";
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
namespace MyOffice.Web.Auth;
|
||||
|
||||
using DbContext;
|
||||
using Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
|
||||
public sealed class OpenIddictSeeder : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<OpenIddictSeeder> _logger;
|
||||
|
||||
public OpenIddictSeeder(
|
||||
IServiceProvider serviceProvider,
|
||||
IConfiguration configuration,
|
||||
ILogger<OpenIddictSeeder> logger
|
||||
)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||
|
||||
await RegisterScopesAsync(scope.ServiceProvider, cancellationToken);
|
||||
await RegisterClientAsync(scope.ServiceProvider, cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private async Task RegisterScopesAsync(IServiceProvider provider, CancellationToken cancellationToken)
|
||||
{
|
||||
var manager = provider.GetRequiredService<IOpenIddictScopeManager>();
|
||||
|
||||
if (await manager.FindByNameAsync(OpenIddictAuthConstants.ApiScope, cancellationToken) is null)
|
||||
{
|
||||
await manager.CreateAsync(new OpenIddictScopeDescriptor
|
||||
{
|
||||
Name = OpenIddictAuthConstants.ApiScope,
|
||||
DisplayName = OpenIddictAuthConstants.ApiFriendlyName,
|
||||
Resources = { OpenIddictAuthConstants.ApiScope }
|
||||
}, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Created OpenIddict scope {Scope}.", OpenIddictAuthConstants.ApiScope);
|
||||
}
|
||||
|
||||
if (await manager.FindByNameAsync(OpenIddictAuthConstants.RolesScope, cancellationToken) is null)
|
||||
{
|
||||
await manager.CreateAsync(new OpenIddictScopeDescriptor
|
||||
{
|
||||
Name = OpenIddictAuthConstants.RolesScope,
|
||||
DisplayName = "User roles"
|
||||
}, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Created OpenIddict scope {Scope}.", OpenIddictAuthConstants.RolesScope);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegisterClientAsync(IServiceProvider provider, CancellationToken cancellationToken)
|
||||
{
|
||||
var manager = provider.GetRequiredService<IOpenIddictApplicationManager>();
|
||||
var globalSettings = provider.GetRequiredService<GlobalSettings>();
|
||||
|
||||
var redirectUri = BuildRedirectUri(globalSettings.Host);
|
||||
var existing = await manager.FindByClientIdAsync(OpenIddictAuthConstants.SpaClientId, cancellationToken);
|
||||
var descriptor = CreateSpaClientDescriptor(redirectUri);
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
await manager.CreateAsync(descriptor, cancellationToken);
|
||||
_logger.LogInformation("Created OpenIddict client {ClientId}.", OpenIddictAuthConstants.SpaClientId);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentRedirectUris = await manager.GetRedirectUrisAsync(existing, cancellationToken);
|
||||
foreach (var uri in currentRedirectUris)
|
||||
{
|
||||
if (!string.Equals(uri, redirectUri.ToString(), StringComparison.Ordinal))
|
||||
descriptor.RedirectUris.Add(new Uri(uri, UriKind.Absolute));
|
||||
}
|
||||
|
||||
await manager.UpdateAsync(existing, descriptor, cancellationToken);
|
||||
_logger.LogInformation("Updated OpenIddict client {ClientId}.", OpenIddictAuthConstants.SpaClientId);
|
||||
}
|
||||
|
||||
internal static OpenIddictApplicationDescriptor CreateSpaClientDescriptor(Uri redirectUri)
|
||||
{
|
||||
var descriptor = new OpenIddictApplicationDescriptor
|
||||
{
|
||||
ClientId = OpenIddictAuthConstants.SpaClientId,
|
||||
DisplayName = "MyOffice SPA",
|
||||
ClientType = ClientTypes.Public,
|
||||
ConsentType = ConsentTypes.Implicit,
|
||||
Permissions =
|
||||
{
|
||||
Permissions.Endpoints.Authorization,
|
||||
Permissions.Endpoints.Token,
|
||||
Permissions.Endpoints.EndSession,
|
||||
Permissions.GrantTypes.AuthorizationCode,
|
||||
Permissions.GrantTypes.Password,
|
||||
Permissions.GrantTypes.RefreshToken,
|
||||
Permissions.Prefixes.GrantType + OpenIddictAuthConstants.ExternalGrantType,
|
||||
Permissions.ResponseTypes.Code,
|
||||
Permissions.Scopes.Email,
|
||||
Permissions.Scopes.Profile,
|
||||
Permissions.Scopes.Roles,
|
||||
Permissions.Prefixes.Scope + Scopes.OpenId,
|
||||
Permissions.Prefixes.Scope + Scopes.OfflineAccess,
|
||||
Permissions.Prefixes.Scope + OpenIddictAuthConstants.ApiScope,
|
||||
Permissions.Prefixes.Scope + OpenIddictAuthConstants.RolesScope
|
||||
}
|
||||
};
|
||||
|
||||
descriptor.RedirectUris.Add(redirectUri);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
internal static Uri BuildRedirectUri(string? host)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
return new Uri("http://localhost:4300/silent-refresh.html");
|
||||
|
||||
return new Uri($"{host.TrimEnd('/')}/silent-refresh.html");
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
namespace MyOffice.Web.Auth;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using DbContext;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
using static OpenIddict.Server.OpenIddictServerEvents;
|
||||
|
||||
public static class OpenIddictServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddMyOfficeOpenIddict(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
IHostEnvironment environment
|
||||
)
|
||||
{
|
||||
services.AddOpenIddict()
|
||||
.AddCore(options =>
|
||||
{
|
||||
options.UseEntityFrameworkCore()
|
||||
.UseDbContext<AppDbContext>();
|
||||
})
|
||||
.AddServer(options =>
|
||||
{
|
||||
options.SetAuthorizationEndpointUris("/connect/authorize")
|
||||
.SetTokenEndpointUris("/connect/token")
|
||||
.SetUserInfoEndpointUris("/connect/userinfo")
|
||||
.SetEndSessionEndpointUris("/connect/logout");
|
||||
|
||||
options.AllowAuthorizationCodeFlow()
|
||||
.AllowPasswordFlow()
|
||||
.AllowRefreshTokenFlow()
|
||||
.AllowCustomFlow(OpenIddictAuthConstants.ExternalGrantType);
|
||||
|
||||
options.RegisterScopes(
|
||||
Scopes.OpenId,
|
||||
Scopes.Email,
|
||||
Scopes.Profile,
|
||||
Scopes.Roles,
|
||||
Scopes.OfflineAccess,
|
||||
OpenIddictAuthConstants.ApiScope,
|
||||
OpenIddictAuthConstants.RolesScope);
|
||||
|
||||
ConfigureCryptography(options, configuration, environment);
|
||||
|
||||
var aspNetCoreBuilder = options.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableUserInfoEndpointPassthrough()
|
||||
.EnableEndSessionEndpointPassthrough();
|
||||
|
||||
// Local HTTP only. In production nginx terminates TLS and
|
||||
// ForwardedHeaders restores https:// for OpenIddict.
|
||||
// Docker demo also serves plain HTTP (OpenIddict:AllowHttp / Environment=Docker).
|
||||
if (environment.IsDevelopment()
|
||||
|| IsDockerEnvironment(environment)
|
||||
|| configuration.GetValue("OpenIddict:AllowHttp", false))
|
||||
{
|
||||
aspNetCoreBuilder.DisableTransportSecurityRequirement();
|
||||
}
|
||||
|
||||
// Password and external grants need custom handlers (separate registrations —
|
||||
// a second UseScopedHandler() on the same builder replaces the first).
|
||||
options.AddEventHandler<HandleTokenRequestContext>(builder =>
|
||||
builder.UseScopedHandler<PasswordGrantHandler>());
|
||||
|
||||
options.AddEventHandler<HandleTokenRequestContext>(builder =>
|
||||
builder.UseScopedHandler<ExternalGrantHandler>());
|
||||
})
|
||||
.AddValidation(options =>
|
||||
{
|
||||
options.UseLocalServer();
|
||||
options.UseAspNetCore();
|
||||
});
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
|
||||
});
|
||||
|
||||
services.AddHostedService<OpenIddictSeeder>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void ConfigureCryptography(
|
||||
OpenIddictServerBuilder options,
|
||||
IConfiguration configuration,
|
||||
IHostEnvironment environment
|
||||
)
|
||||
{
|
||||
// Dev: certs in the user profile. Docker/demo: ephemeral keys (no cert store in containers).
|
||||
if (string.IsNullOrWhiteSpace(configuration["OpenIddict:SigningKeyPath"])
|
||||
&& (environment.IsDevelopment()
|
||||
|| IsDockerEnvironment(environment)
|
||||
|| configuration.GetValue("OpenIddict:UseEphemeralKeys", false)))
|
||||
{
|
||||
if (IsDockerEnvironment(environment)
|
||||
|| configuration.GetValue("OpenIddict:UseEphemeralKeys", false))
|
||||
{
|
||||
options.AddEphemeralEncryptionKey()
|
||||
.AddEphemeralSigningKey();
|
||||
}
|
||||
else
|
||||
{
|
||||
options.AddDevelopmentEncryptionCertificate()
|
||||
.AddDevelopmentSigningCertificate();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var contentRoot = configuration.GetValue<string>(WebHostDefaults.ContentRootKey)
|
||||
?? throw new InvalidOperationException("Content root path is not configured.");
|
||||
|
||||
var signingPath = ResolveKeyPath(
|
||||
configuration["OpenIddict:SigningKeyPath"],
|
||||
contentRoot,
|
||||
"keys/signing.pem");
|
||||
var encryptionPath = ResolveKeyPath(
|
||||
configuration["OpenIddict:EncryptionKeyPath"],
|
||||
contentRoot,
|
||||
"keys/encryption.pem");
|
||||
|
||||
if (!File.Exists(signingPath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"OpenIddict signing key not found at '{signingPath}'. " +
|
||||
"Set OpenIddict:SigningKeyPath or place keys/signing.pem under the content root.");
|
||||
}
|
||||
|
||||
options.AddSigningKey(LoadRsaSecurityKey(signingPath));
|
||||
|
||||
if (File.Exists(encryptionPath) &&
|
||||
!string.Equals(encryptionPath, signingPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.AddEncryptionKey(LoadRsaSecurityKey(encryptionPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prefer a dedicated encryption key. Fallback keeps older single-key setups working.
|
||||
options.AddEncryptionKey(LoadRsaSecurityKey(signingPath));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDockerEnvironment(IHostEnvironment environment) =>
|
||||
string.Equals(environment.EnvironmentName, "Docker", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string ResolveKeyPath(string? configuredPath, string contentRoot, string defaultRelative)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(configuredPath))
|
||||
{
|
||||
return Path.IsPathRooted(configuredPath)
|
||||
? configuredPath
|
||||
: Path.GetFullPath(Path.Combine(contentRoot, configuredPath));
|
||||
}
|
||||
|
||||
return Path.GetFullPath(Path.Combine(contentRoot, defaultRelative));
|
||||
}
|
||||
|
||||
private static RsaSecurityKey LoadRsaSecurityKey(string path)
|
||||
{
|
||||
var privateKey = File.ReadAllText(path)
|
||||
.Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("-----END RSA PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("-----END PRIVATE KEY-----", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("\r", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("\n", string.Empty, StringComparison.Ordinal);
|
||||
|
||||
var rsa = RSA.Create();
|
||||
var keyBytes = Convert.FromBase64String(privateKey);
|
||||
|
||||
try
|
||||
{
|
||||
rsa.ImportRSAPrivateKey(keyBytes, out _);
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
rsa.ImportPkcs8PrivateKey(keyBytes, out _);
|
||||
}
|
||||
|
||||
return new RsaSecurityKey(rsa);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
namespace MyOffice.Web.Auth;
|
||||
|
||||
using Identity.Domain;
|
||||
using Identity.Repositories;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using static OpenIddict.Abstractions.OpenIddictConstants;
|
||||
using static OpenIddict.Server.OpenIddictServerEvents;
|
||||
|
||||
public sealed class PasswordGrantHandler : IOpenIddictServerHandler<HandleTokenRequestContext>
|
||||
{
|
||||
private readonly AppUserManager _userManager;
|
||||
private readonly SignInManager<ApplicationUser<Guid>> _signInManager;
|
||||
|
||||
public PasswordGrantHandler(
|
||||
AppUserManager userManager,
|
||||
SignInManager<ApplicationUser<Guid>> signInManager
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public async ValueTask HandleAsync(HandleTokenRequestContext context)
|
||||
{
|
||||
if (!string.Equals(context.Request.GrantType, GrantTypes.Password, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var username = context.Request.Username?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Invalid username or password.");
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByNameAsync(username)
|
||||
?? await _userManager.FindByEmailAsync(username);
|
||||
if (user is null)
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Invalid username or password.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _signInManager.CanSignInAsync(user))
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "The specified user cannot sign in.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "The specified user is locked out.");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _signInManager.CheckPasswordSignInAsync(user, context.Request.Password!, lockoutOnFailure: true);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
context.Reject(Errors.InvalidGrant, "Invalid username or password.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_userManager.SupportsUserLockout)
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
|
||||
context.SignIn(OpenIddictClaimsHelper.CreatePrincipal(user, context.Request.GetScopes()));
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
namespace MyOffice.Web.Identity.Repositories;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using Domain;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Identity V3 hasher for new passwords; still verifies the legacy
|
||||
/// (16-byte salt + 20-byte PBKDF2-SHA1 @ 100k) format and signals rehash.
|
||||
/// </summary>
|
||||
public class PasswordHasher : IPasswordHasher<ApplicationUser<Guid>>
|
||||
{
|
||||
private const int LegacySaltSize = 16;
|
||||
private const int LegacyHashSize = 20;
|
||||
private const int LegacyIterations = 100_000;
|
||||
private const int LegacyPayloadSize = LegacySaltSize + LegacyHashSize;
|
||||
|
||||
private readonly PasswordHasher<ApplicationUser<Guid>> _identityHasher = new();
|
||||
|
||||
public string HashPassword(ApplicationUser<Guid> user, string password) =>
|
||||
_identityHasher.HashPassword(user, password);
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(
|
||||
ApplicationUser<Guid> user,
|
||||
string hashedPassword,
|
||||
string providedPassword
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hashedPassword) || providedPassword == null)
|
||||
{
|
||||
return PasswordVerificationResult.Failed;
|
||||
}
|
||||
|
||||
// Prefer modern Identity format when payload is not the legacy 36-byte blob.
|
||||
if (!IsLegacyPayload(hashedPassword))
|
||||
{
|
||||
return _identityHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
}
|
||||
|
||||
if (VerifyLegacyHash(providedPassword, hashedPassword))
|
||||
{
|
||||
return PasswordVerificationResult.SuccessRehashNeeded;
|
||||
}
|
||||
|
||||
return PasswordVerificationResult.Failed;
|
||||
}
|
||||
|
||||
private static bool IsLegacyPayload(string hashedPassword)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(hashedPassword);
|
||||
return bytes.Length == LegacyPayloadSize;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string HashLegacyForTests(string password)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(LegacySaltSize);
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
LegacyIterations,
|
||||
HashAlgorithmName.SHA1,
|
||||
LegacyHashSize);
|
||||
|
||||
var payload = new byte[LegacyPayloadSize];
|
||||
Buffer.BlockCopy(salt, 0, payload, 0, LegacySaltSize);
|
||||
Buffer.BlockCopy(hash, 0, payload, LegacySaltSize, LegacyHashSize);
|
||||
return Convert.ToBase64String(payload);
|
||||
}
|
||||
|
||||
private static bool VerifyLegacyHash(string password, string passwordHash)
|
||||
{
|
||||
byte[] hashBytes;
|
||||
try
|
||||
{
|
||||
hashBytes = Convert.FromBase64String(passwordHash);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hashBytes.Length != LegacyPayloadSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var salt = hashBytes.AsSpan(0, LegacySaltSize);
|
||||
var expected = hashBytes.AsSpan(LegacySaltSize, LegacyHashSize);
|
||||
var actual = Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
LegacyIterations,
|
||||
HashAlgorithmName.SHA1,
|
||||
LegacyHashSize);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
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 Task<IdentityResult> CreateAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = _userRepository.AddUser(new User
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
PasswordHash = user.PasswordHash,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
FullName = user.FullName,
|
||||
CurrencyId = user.CurrencyId!,
|
||||
IsEmailConfirmed = user.IsEmailConfirmed
|
||||
});
|
||||
|
||||
return Task.FromResult(result == 1 ? IdentityResult.Success : IdentityResult.Failed());
|
||||
}
|
||||
|
||||
public 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,
|
||||
CurrencyId = user.CurrencyId!,
|
||||
IsEmailConfirmed = user.IsEmailConfirmed
|
||||
};
|
||||
|
||||
var result = _userRepository.UpdateUser(userDb);
|
||||
return Task.FromResult(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,
|
||||
CurrencyId = user.CurrencyId,
|
||||
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,
|
||||
CurrencyId = user.CurrencyId,
|
||||
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)
|
||||
{
|
||||
return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash));
|
||||
}
|
||||
|
||||
public Task SetEmailAsync(ApplicationUser<Guid> user, string email, CancellationToken cancellationToken)
|
||||
{
|
||||
user.Email = email;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> GetEmailAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<bool> GetEmailConfirmedAsync(ApplicationUser<Guid> user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.IsEmailConfirmed);
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(ApplicationUser<Guid> user, bool confirmed, CancellationToken cancellationToken)
|
||||
{
|
||||
user.IsEmailConfirmed = confirmed;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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