Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

ExternalAuthenticationService.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Localization;
using Nop.Core.Events;
using Nop.Core.Http.Extensions;
using Nop.Data;
using Nop.Services.Common;
using Nop.Services.Customers;
using Nop.Services.Localization;
using Nop.Services.Messages;

namespace Nop.Services.Authentication.External;

/// 
/// Represents external authentication service implementation
/// 
public partial class ExternalAuthenticationService : IExternalAuthenticationService
{
    #region Fields

    protected readonly CustomerSettings _customerSettings;
    protected readonly ExternalAuthenticationSettings _externalAuthenticationSettings;
    protected readonly IActionContextAccessor _actionContextAccessor;
    protected readonly IAuthenticationPluginManager _authenticationPluginManager;
    protected readonly ICustomerRegistrationService _customerRegistrationService;
    protected readonly ICustomerService _customerService;
    protected readonly IEventPublisher _eventPublisher;
    protected readonly IGenericAttributeService _genericAttributeService;
    protected readonly IHttpContextAccessor _httpContextAccessor;
    protected readonly ILocalizationService _localizationService;
    protected readonly IRepository _externalAuthenticationRecordRepository;
    protected readonly IStoreContext _storeContext;
    protected readonly IUrlHelperFactory _urlHelperFactory;
    protected readonly IWorkContext _workContext;
    protected readonly IWorkflowMessageService _workflowMessageService;
    protected readonly LocalizationSettings _localizationSettings;

    #endregion

    #region Ctor

    public ExternalAuthenticationService(CustomerSettings customerSettings,
        ExternalAuthenticationSettings externalAuthenticationSettings,
        IActionContextAccessor actionContextAccessor,
        IAuthenticationPluginManager authenticationPluginManager,
        ICustomerRegistrationService customerRegistrationService,
        ICustomerService customerService,
        IEventPublisher eventPublisher,
        IGenericAttributeService genericAttributeService,
        IHttpContextAccessor httpContextAccessor,
        ILocalizationService localizationService,
        IRepository externalAuthenticationRecordRepository,
        IStoreContext storeContext,
        IUrlHelperFactory urlHelperFactory,
        IWorkContext workContext,
        IWorkflowMessageService workflowMessageService,
        LocalizationSettings localizationSettings)
    {
        _customerSettings = customerSettings;
        _externalAuthenticationSettings = externalAuthenticationSettings;
        _actionContextAccessor = actionContextAccessor;
        _authenticationPluginManager = authenticationPluginManager;
        _customerRegistrationService = customerRegistrationService;
        _customerService = customerService;
        _eventPublisher = eventPublisher;
        _genericAttributeService = genericAttributeService;
        _httpContextAccessor = httpContextAccessor;
        _localizationService = localizationService;
        _externalAuthenticationRecordRepository = externalAuthenticationRecordRepository;
        _storeContext = storeContext;
        _urlHelperFactory = urlHelperFactory;
        _workContext = workContext;
        _workflowMessageService = workflowMessageService;
        _localizationSettings = localizationSettings;
    }

    #endregion

    #region Utilities

    /// 
    /// Authenticate user with existing associated external account
    /// 
    /// Associated with passed external authentication parameters user
    /// Current logged-in user
    /// URL to which the user will return after authentication
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// 
    protected virtual async Task AuthenticateExistingUserAsync(Customer associatedUser, Customer currentLoggedInUser, string returnUrl)
    {
        //log in guest user
        if (currentLoggedInUser == null)
            return await _customerRegistrationService.SignInCustomerAsync(associatedUser, returnUrl);

        //account is already assigned to another user
        if (currentLoggedInUser.Id != associatedUser.Id)
            return await ErrorAuthenticationAsync(new[]
            {
                await _localizationService.GetResourceAsync("Account.AssociatedExternalAuth.AccountAlreadyAssigned")
            }, returnUrl);

        //or the user try to log in as himself. bit weird
        return SuccessfulAuthentication(returnUrl);
    }

    /// 
    /// Authenticate current user and associate new external account with user
    /// 
    /// Current logged-in user
    /// Authentication parameters received from external authentication method
    /// URL to which the user will return after authentication
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// 
    protected virtual async Task AuthenticateNewUserAsync(Customer currentLoggedInUser, ExternalAuthenticationParameters parameters, string returnUrl)
    {
        //associate external account with logged-in user
        if (currentLoggedInUser != null)
        {
            await AssociateExternalAccountWithUserAsync(currentLoggedInUser, parameters);

            return SuccessfulAuthentication(returnUrl);
        }

        //or try to register new user
        if (_customerSettings.UserRegistrationType != UserRegistrationType.Disabled)
            return await RegisterNewUserAsync(parameters, returnUrl);

        //registration is disabled
        return await ErrorAuthenticationAsync(new[] { "Registration is disabled" }, returnUrl);
    }

    /// 
    /// Register new user
    /// 
    /// Authentication parameters received from external authentication method
    /// URL to which the user will return after authentication
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// 
    protected virtual async Task RegisterNewUserAsync(ExternalAuthenticationParameters parameters, string returnUrl)
    {
        //check whether the specified email has been already registered
        if (await _customerService.GetCustomerByEmailAsync(parameters.Email) != null)
        {
            var alreadyExistsError = string.Format(await _localizationService.GetResourceAsync("Account.AssociatedExternalAuth.EmailAlreadyExists"),
                !string.IsNullOrEmpty(parameters.ExternalDisplayIdentifier) ? parameters.ExternalDisplayIdentifier : parameters.ExternalIdentifier);
            return await ErrorAuthenticationAsync(new[] { alreadyExistsError }, returnUrl);
        }

        //registration is approved if validation isn't required
        var registrationIsApproved = _customerSettings.UserRegistrationType == UserRegistrationType.Standard ||
                                     (_customerSettings.UserRegistrationType == UserRegistrationType.EmailValidation && !_externalAuthenticationSettings.RequireEmailValidation);

        //create registration request
        var customer = await _workContext.GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();
        var registrationRequest = new CustomerRegistrationRequest(customer,
            parameters.Email, parameters.Email,
            CommonHelper.GenerateRandomDigitCode(20),
            PasswordFormat.Hashed,
            store.Id,
            registrationIsApproved);

        //whether registration request has been completed successfully
        var registrationResult = await _customerRegistrationService.RegisterCustomerAsync(registrationRequest);
        if (!registrationResult.Success)
            return await ErrorAuthenticationAsync(registrationResult.Errors, returnUrl);

        //allow to save other customer values by consuming this event
        await _eventPublisher.PublishAsync(new CustomerAutoRegisteredByExternalMethodEvent(customer, parameters));

        //raise customer registered event
        await _eventPublisher.PublishAsync(new CustomerRegisteredEvent(customer));

        //store owner notifications
        if (_customerSettings.NotifyNewCustomerRegistration)
            await _workflowMessageService.SendCustomerRegisteredStoreOwnerNotificationMessageAsync(customer, _localizationSettings.DefaultAdminLanguageId);

        //associate external account with registered user
        await AssociateExternalAccountWithUserAsync(customer, parameters);

        //authenticate
        var currentLanguage = await _workContext.GetWorkingLanguageAsync();
        if (registrationIsApproved)
        {
            await _workflowMessageService.SendCustomerWelcomeMessageAsync(customer, currentLanguage.Id);

            //raise event       
            await _eventPublisher.PublishAsync(new CustomerActivatedEvent(customer));

            return await _customerRegistrationService.SignInCustomerAsync(customer, returnUrl, true);
        }

        //registration is succeeded but isn't activated
        if (_customerSettings.UserRegistrationType == UserRegistrationType.EmailValidation)
        {
            //email validation message
            await _genericAttributeService.SaveAttributeAsync(customer, NopCustomerDefaults.AccountActivationTokenAttribute, Guid.NewGuid().ToString());
            await _workflowMessageService.SendCustomerEmailValidationMessageAsync(customer, currentLanguage.Id);

            return new RedirectToRouteResult("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl });
        }

        //registration is succeeded but isn't approved by admin
        if (_customerSettings.UserRegistrationType == UserRegistrationType.AdminApproval)
            return new RedirectToRouteResult("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl });

        return await ErrorAuthenticationAsync(new[] { "Error on registration" }, returnUrl);
    }

    /// 
    /// Add errors that occurred during authentication
    /// 
    /// Collection of errors
    /// URL to which the user will return after authentication
    /// Result of an authentication
    protected virtual async Task ErrorAuthenticationAsync(IEnumerable errors, string returnUrl)
    {
        var session = _httpContextAccessor.HttpContext?.Session;

        if (session != null)
        {
            var existsErrors = (await session.GetAsync>(NopAuthenticationDefaults.ExternalAuthenticationErrorsSessionKey))?.ToList() ?? new List();

            existsErrors.AddRange(errors);

            await session.SetAsync(NopAuthenticationDefaults.ExternalAuthenticationErrorsSessionKey, existsErrors);
        }

        return new RedirectToActionResult("Login", "Customer", !string.IsNullOrEmpty(returnUrl) ? new { ReturnUrl = returnUrl } : null);
    }

    /// 
    /// Redirect the user after successful authentication
    /// 
    /// URL to which the user will return after authentication
    /// Result of an authentication
    protected virtual IActionResult SuccessfulAuthentication(string returnUrl)
    {
        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);

        //redirect to the return URL if it's specified
        if (!string.IsNullOrEmpty(returnUrl) && urlHelper.IsLocalUrl(returnUrl))
            return new RedirectResult(returnUrl);

        return new RedirectToRouteResult("Homepage", null);
    }

    #endregion

    #region Methods

    #region Authentication

    /// 
    /// Authenticate user by passed parameters
    /// 
    /// External authentication parameters
    /// URL to which the user will return after authentication
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result of an authentication
    /// 
    public virtual async Task AuthenticateAsync(ExternalAuthenticationParameters parameters, string returnUrl = null)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var customer = await _workContext.GetCurrentCustomerAsync();
        var store = await _storeContext.GetCurrentStoreAsync();
        if (!await _authenticationPluginManager.IsPluginActiveAsync(parameters.ProviderSystemName, customer, store.Id))
            return await ErrorAuthenticationAsync(new[] { "External authentication method cannot be loaded" }, returnUrl);

        //get current logged-in user
        var currentLoggedInUser = await _customerService.IsRegisteredAsync(customer) ? customer : null;

        //authenticate associated user if already exists
        var associatedUser = await GetUserByExternalAuthenticationParametersAsync(parameters);
        if (associatedUser != null)
            return await AuthenticateExistingUserAsync(associatedUser, currentLoggedInUser, returnUrl);

        //or associate and authenticate new user
        return await AuthenticateNewUserAsync(currentLoggedInUser, parameters, returnUrl);
    }

    #endregion

    /// 
    /// Get the external authentication records by identifier
    /// 
    /// External authentication record identifier
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// 
    public virtual async Task GetExternalAuthenticationRecordByIdAsync(int externalAuthenticationRecordId)
    {
        return await _externalAuthenticationRecordRepository.GetByIdAsync(externalAuthenticationRecordId, cache => default, useShortTermCache: true);
    }

    /// 
    /// Get list of the external authentication records by customer
    /// 
    /// Customer
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// 
    public virtual async Task> GetCustomerExternalAuthenticationRecordsAsync(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var associationRecords = _externalAuthenticationRecordRepository.Table.Where(ear => ear.CustomerId == customer.Id);

        return await associationRecords.ToListAsync();
    }

    /// 
    /// Delete the external authentication record
    /// 
    /// External authentication record
    /// A task that represents the asynchronous operation
    public virtual async Task DeleteExternalAuthenticationRecordAsync(ExternalAuthenticationRecord externalAuthenticationRecord)
    {
        ArgumentNullException.ThrowIfNull(externalAuthenticationRecord);

        await _externalAuthenticationRecordRepository.DeleteAsync(externalAuthenticationRecord, false);
    }

    /// 
    /// Get the external authentication record
    /// 
    /// External authentication parameters
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the result
    /// 
    public virtual async Task GetExternalAuthenticationRecordByExternalAuthenticationParametersAsync(ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var associationRecord = await _externalAuthenticationRecordRepository.Table.FirstOrDefaultAsync(record =>
            record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));

        return associationRecord;
    }

    /// 
    /// Associate external account with customer
    /// 
    /// Customer
    /// External authentication parameters
    /// A task that represents the asynchronous operation
    public virtual async Task AssociateExternalAccountWithUserAsync(Customer customer, ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(customer);

        var externalAuthenticationRecord = new ExternalAuthenticationRecord
        {
            CustomerId = customer.Id,
            Email = parameters.Email,
            ExternalIdentifier = parameters.ExternalIdentifier,
            ExternalDisplayIdentifier = parameters.ExternalDisplayIdentifier,
            OAuthAccessToken = parameters.AccessToken,
            ProviderSystemName = parameters.ProviderSystemName
        };

        await _externalAuthenticationRecordRepository.InsertAsync(externalAuthenticationRecord, false);
    }

    /// 
    /// Get the particular user with specified parameters
    /// 
    /// External authentication parameters
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the customer
    /// 
    public virtual async Task GetUserByExternalAuthenticationParametersAsync(ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var associationRecord = _externalAuthenticationRecordRepository.Table.FirstOrDefault(record =>
            record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));
        if (associationRecord == null)
            return null;

        return await _customerService.GetCustomerByIdAsync(associationRecord.CustomerId);
    }

    /// 
    /// Remove the association
    /// 
    /// External authentication parameters
    /// A task that represents the asynchronous operation
    public virtual async Task RemoveAssociationAsync(ExternalAuthenticationParameters parameters)
    {
        ArgumentNullException.ThrowIfNull(parameters);

        var associationRecord = await _externalAuthenticationRecordRepository.Table.FirstOrDefaultAsync(record =>
            record.ExternalIdentifier.Equals(parameters.ExternalIdentifier) && record.ProviderSystemName.Equals(parameters.ProviderSystemName));

        if (associationRecord != null)
            await _externalAuthenticationRecordRepository.DeleteAsync(associationRecord, false);
    }


    #endregion
}