diff --git a/.editorconfig b/.editorconfig index 52a5b14..d467f1b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -102,7 +102,7 @@ csharp_preserve_single_line_statements = false csharp_preserve_single_line_blocks = true csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion -csharp_style_namespace_declarations = block_scoped:silent +csharp_style_namespace_declarations = file_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_expression_bodied_lambdas = true:silent diff --git a/src/VirtoCommerce.Xapi.Core/Models/StoreSettings.cs b/src/VirtoCommerce.Xapi.Core/Models/StoreSettings.cs index 7c0569a..bf80d07 100644 --- a/src/VirtoCommerce.Xapi.Core/Models/StoreSettings.cs +++ b/src/VirtoCommerce.Xapi.Core/Models/StoreSettings.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.AspNetCore.Identity; namespace VirtoCommerce.Xapi.Core.Models @@ -28,6 +29,8 @@ public class StoreSettings public PasswordOptions PasswordRequirements { get; set; } + public IList AuthenticationTypes { get; set; } + public ModuleSettings[] Modules { get; set; } } } diff --git a/src/VirtoCommerce.Xapi.Core/Schemas/StoreSettingsType.cs b/src/VirtoCommerce.Xapi.Core/Schemas/StoreSettingsType.cs index 8e4516f..f6a6301 100644 --- a/src/VirtoCommerce.Xapi.Core/Schemas/StoreSettingsType.cs +++ b/src/VirtoCommerce.Xapi.Core/Schemas/StoreSettingsType.cs @@ -19,6 +19,7 @@ public StoreSettingsType() Field(x => x.DefaultSelectedForCheckout).Description("Default \"Selected for checkout\" state for new line items and gifts"); Field(x => x.EnvironmentName).Description("Environment name"); Field("passwordRequirements", "Password requirements"); + Field>>("authenticationTypes"); Field>>>("modules"); } } diff --git a/src/VirtoCommerce.Xapi.Core/Services/IUserManagerCore.cs b/src/VirtoCommerce.Xapi.Core/Services/IUserManagerCore.cs index b46c380..8b579f1 100644 --- a/src/VirtoCommerce.Xapi.Core/Services/IUserManagerCore.cs +++ b/src/VirtoCommerce.Xapi.Core/Services/IUserManagerCore.cs @@ -1,4 +1,6 @@ +using System; using System.Threading.Tasks; +using GraphQL; using VirtoCommerce.Platform.Core.Security; namespace VirtoCommerce.Xapi.Core.Services @@ -7,6 +9,9 @@ public interface IUserManagerCore { Task IsLockedOutAsync(ApplicationUser user); + [Obsolete("Use CheckCurrentUserState()", DiagnosticId = "VC0009", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions/")] Task CheckUserState(string userId, bool allowAnonymous); + + Task CheckCurrentUserState(IResolveFieldContext context, bool allowAnonymous); } } diff --git a/src/VirtoCommerce.Xapi.Core/VirtoCommerce.Xapi.Core.csproj b/src/VirtoCommerce.Xapi.Core/VirtoCommerce.Xapi.Core.csproj index 26bac04..11d4e66 100644 --- a/src/VirtoCommerce.Xapi.Core/VirtoCommerce.Xapi.Core.csproj +++ b/src/VirtoCommerce.Xapi.Core/VirtoCommerce.Xapi.Core.csproj @@ -32,7 +32,7 @@ - + \ No newline at end of file diff --git a/src/VirtoCommerce.Xapi.Data/Extensions/ServiceCollectionExtensions.cs b/src/VirtoCommerce.Xapi.Data/Extensions/ServiceCollectionExtensions.cs index bf53f39..46ef422 100644 --- a/src/VirtoCommerce.Xapi.Data/Extensions/ServiceCollectionExtensions.cs +++ b/src/VirtoCommerce.Xapi.Data/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; using VirtoCommerce.Platform.Security.OpenIddict; using VirtoCommerce.Xapi.Core; using VirtoCommerce.Xapi.Core.Extensions; @@ -52,6 +53,8 @@ public static IServiceCollection AddXCore(this IServiceCollection services, IGra services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // provider for external fields services.AddSingleton(); diff --git a/src/VirtoCommerce.Xapi.Data/Queries/GetStoreQueryHandler.cs b/src/VirtoCommerce.Xapi.Data/Queries/GetStoreQueryHandler.cs index 8098644..796bb9b 100644 --- a/src/VirtoCommerce.Xapi.Data/Queries/GetStoreQueryHandler.cs +++ b/src/VirtoCommerce.Xapi.Data/Queries/GetStoreQueryHandler.cs @@ -29,6 +29,7 @@ public class GetStoreQueryHandler : IQueryHandler private readonly IdentityOptions _identityOptions; private readonly GraphQLWebSocketOptions _webSocketOptions; private readonly StoresOptions _storeOptions; + private readonly IStoreAuthenticationService _storeAuthenticationService; public GetStoreQueryHandler( IStoreService storeService, @@ -37,13 +38,15 @@ public GetStoreQueryHandler( ISettingsManager settingsManager, IOptions identityOptions, IOptions webSocketOptions, - IOptions storeOptions) + IOptions storeOptions, + IStoreAuthenticationService storeAuthenticationService) { _storeService = storeService; _storeSearchService = storeSearchService; _storeCurrencyResolver = storeCurrencyResolver; _settingsManager = settingsManager; + _storeAuthenticationService = storeAuthenticationService; _identityOptions = identityOptions.Value; _webSocketOptions = webSocketOptions.Value; _storeOptions = storeOptions.Value; @@ -116,6 +119,11 @@ public async Task Handle(GetStoreQuery request, CancellationToken EnvironmentName = _settingsManager.GetValue(ModuleConstants.Settings.General.EnvironmentName), PasswordRequirements = _identityOptions.Password, + AuthenticationTypes = (await _storeAuthenticationService.GetStoreSchemesAsync(store.Id)) + .Where(x => x.IsActive) + .Select(x => x.Name) + .ToList(), + Modules = ToModulesSettings(store.Settings) }; } diff --git a/src/VirtoCommerce.Xapi.Data/Services/ExternalSignInUserBuilder.cs b/src/VirtoCommerce.Xapi.Data/Services/ExternalSignInUserBuilder.cs new file mode 100644 index 0000000..6f957a1 --- /dev/null +++ b/src/VirtoCommerce.Xapi.Data/Services/ExternalSignInUserBuilder.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using VirtoCommerce.CustomerModule.Core.Model; +using VirtoCommerce.CustomerModule.Core.Services; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Security; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; +using VirtoCommerce.Platform.Core.Settings; +using VirtoCommerce.StoreModule.Core.Services; +using CustomerSettings = VirtoCommerce.CustomerModule.Core.ModuleConstants.Settings.General; + +namespace VirtoCommerce.Xapi.Data.Services; + +public class ExternalSignInUserBuilder(IStoreService storeService, IMemberService memberService) : IExternalSignInUserBuilder +{ + public async Task BuildNewUser(ApplicationUser user, ExternalLoginInfo externalLoginInfo) + { + if (user.MemberId is null && user.UserType == UserType.Customer.ToString()) + { + var contact = AbstractTypeFactory.TryCreateInstance(); + contact.Name = externalLoginInfo.Principal.FindFirstValue("name"); + contact.FirstName = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.GivenName); + contact.LastName = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Surname); + contact.Emails = [user.Email]; + + if (!string.IsNullOrEmpty(user.StoreId)) + { + var store = await storeService.GetNoCloneAsync(user.StoreId); + contact.Status = store?.Settings.GetValue(CustomerSettings.ContactDefaultStatus); + } + + await memberService.SaveChangesAsync([contact]); + + user.MemberId = contact.Id; + } + } +} diff --git a/src/VirtoCommerce.Xapi.Data/Services/ExternalSignInValidator.cs b/src/VirtoCommerce.Xapi.Data/Services/ExternalSignInValidator.cs new file mode 100644 index 0000000..25a602a --- /dev/null +++ b/src/VirtoCommerce.Xapi.Data/Services/ExternalSignInValidator.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using VirtoCommerce.Platform.Core.Common; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; +using VirtoCommerce.StoreModule.Core.Services; + +namespace VirtoCommerce.Xapi.Data.Services; + +public class ExternalSignInValidator(IStoreService storeService, + IStoreAuthenticationService storeAuthenticationService) + : IExternalSignInValidator +{ + public async Task ValidateAsync(ExternalSignInRequest request) + { + var store = await storeService.GetNoCloneAsync(request.StoreId); + + if (store is null || + string.IsNullOrEmpty(store.Url) || + !Uri.TryCreate(store.Url, UriKind.Absolute, out var storeUri) || + !IsValidUrl(storeUri, request.OidcUrl) || + !IsValidUrl(storeUri, request.CallbackUrl)) + { + return false; + } + + var schemes = await storeAuthenticationService.GetStoreSchemesAsync(request.StoreId, clone: false); + + return schemes.Any(x => x.IsActive && x.Name.EqualsIgnoreCase(request.AuthenticationType)); + } + + private static bool IsValidUrl(Uri baseUri, string uriString) + { + return Uri.TryCreate(uriString, UriKind.Absolute, out var uri) && baseUri.IsBaseOf(uri); + } +} diff --git a/src/VirtoCommerce.Xapi.Data/Services/UserManagerCore.cs b/src/VirtoCommerce.Xapi.Data/Services/UserManagerCore.cs index 0d59c94..cfeb7ff 100644 --- a/src/VirtoCommerce.Xapi.Data/Services/UserManagerCore.cs +++ b/src/VirtoCommerce.Xapi.Data/Services/UserManagerCore.cs @@ -1,7 +1,10 @@ using System; using System.Threading.Tasks; +using GraphQL; using Microsoft.AspNetCore.Identity; using VirtoCommerce.Platform.Core.Security; +using VirtoCommerce.Platform.Security.Extensions; +using VirtoCommerce.Xapi.Core.Extensions; using VirtoCommerce.Xapi.Core.Security.Authorization; using VirtoCommerce.Xapi.Core.Services; @@ -25,7 +28,23 @@ public virtual async Task IsLockedOutAsync(ApplicationUser user) return result; } - public async Task CheckUserState(string userId, bool allowAnonymous) + [Obsolete("Use CheckCurrentUserState()", DiagnosticId = "VC0009", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions/")] + public Task CheckUserState(string userId, bool allowAnonymous) + { + return CheckUserState(userId, allowAnonymous, isExternalSignIn: false); + } + + public Task CheckCurrentUserState(IResolveFieldContext context, bool allowAnonymous) + { + var principal = context.GetCurrentPrincipal(); + var userId = principal.GetCurrentUserId(); + var isExternalSignIn = principal.IsExternalSignIn(); + + return CheckUserState(userId, allowAnonymous, isExternalSignIn); + } + + + private async Task CheckUserState(string userId, bool allowAnonymous, bool isExternalSignIn) { var userManager = _userManagerFactory(); var user = await userManager.FindByIdAsync(userId); @@ -40,7 +59,7 @@ public async Task CheckUserState(string userId, bool allowAnonymous) throw AuthorizationError.AnonymousAccessDenied(); } - if (user.PasswordExpired) + if (user.PasswordExpired && !isExternalSignIn) { throw AuthorizationError.PasswordExpired(); } diff --git a/src/VirtoCommerce.Xapi.Data/VirtoCommerce.Xapi.Data.csproj b/src/VirtoCommerce.Xapi.Data/VirtoCommerce.Xapi.Data.csproj index b469227..763e393 100644 --- a/src/VirtoCommerce.Xapi.Data/VirtoCommerce.Xapi.Data.csproj +++ b/src/VirtoCommerce.Xapi.Data/VirtoCommerce.Xapi.Data.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/VirtoCommerce.Xapi.Web/VirtoCommerce.Xapi.Web.csproj b/src/VirtoCommerce.Xapi.Web/VirtoCommerce.Xapi.Web.csproj index d315364..4f33e3d 100644 --- a/src/VirtoCommerce.Xapi.Web/VirtoCommerce.Xapi.Web.csproj +++ b/src/VirtoCommerce.Xapi.Web/VirtoCommerce.Xapi.Web.csproj @@ -9,8 +9,6 @@ - - diff --git a/src/VirtoCommerce.Xapi.Web/module.manifest b/src/VirtoCommerce.Xapi.Web/module.manifest index e045c52..d032e67 100644 --- a/src/VirtoCommerce.Xapi.Web/module.manifest +++ b/src/VirtoCommerce.Xapi.Web/module.manifest @@ -4,12 +4,12 @@ 3.802.0 - 3.841.0 + 3.848.0 - +