/// <summary>
            /// Retrieves the transaction service profile.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <returns>The transaction service profile.</returns>
            private static TransactionServiceProfile GetTransactionServiceProfile(Microsoft.Dynamics.Commerce.Runtime.RequestContext context)
            {
                GetTransactionServiceProfileDataRequest request = new GetTransactionServiceProfileDataRequest();

                Microsoft.Dynamics.Commerce.Runtime.RequestContext getTransactionServiceProfileRequestContext;

                // Transaction service profile is associated with channel
                // Decide what context is to be used to retrieve it
                if (context.GetPrincipal().IsChannelAgnostic)
                {
                    // If the context is channel agnostic (no channel information), then use any channel available to retrieve the profile
                    GetChannelIdServiceRequest  getChannelRequest  = new GetChannelIdServiceRequest();
                    GetChannelIdServiceResponse getChannelResponse = context.Execute <GetChannelIdServiceResponse>(getChannelRequest);
                    long anyChannelId = getChannelResponse.ChannelId;

                    var anonymousIdentity = new CommerceIdentity()
                    {
                        ChannelId = anyChannelId
                    };

                    getTransactionServiceProfileRequestContext = new Microsoft.Dynamics.Commerce.Runtime.RequestContext(context.Runtime);
                    getTransactionServiceProfileRequestContext.SetPrincipal(new CommercePrincipal(anonymousIdentity));
                }
                else
                {
                    // If the request has channel information, then use current context to retrieve the transaction service profile
                    getTransactionServiceProfileRequestContext = context;
                }

                return(getTransactionServiceProfileRequestContext.Execute <SingleEntityDataServiceResponse <TransactionServiceProfile> >(request).Entity);
            }
            /// <summary>
            /// Initializes a new instance of the <see cref="TransactionServiceClientFactory"/> class.
            /// </summary>
            /// <param name="context">The request context.</param>
            public TransactionServiceClientFactory(Microsoft.Dynamics.Commerce.Runtime.RequestContext context)
            {
                ThrowIf.Null(context, "context");

                this.dataAreaId = context.GetChannelConfiguration() != null
                                      ? context.GetChannelConfiguration().InventLocationDataAreaId
                                      : string.Empty;

                var getTransactionServiceProfileDataRequest = new GetTransactionServiceProfileDataRequest();

                this.transactionServiceProfile = context.Runtime.Execute <SingleEntityDataServiceResponse <TransactionServiceProfile> >(getTransactionServiceProfileDataRequest, context).Entity;
                this.transactionServiceProfile = GetTransactionServiceProfile(context);
                this.SetRealTimeServiceProfileConfigSettings(context);
                this.requestLanguageId = this.transactionServiceProfile.LanguageId;
                this.ValidateTransactionProfile();
            }
            /// <summary>
            /// Change password for the user.
            /// </summary>
            /// <param name="request">The device activation request.</param>
            /// <returns>The device activation response.</returns>
            private static Response ChangePassword(UserChangePasswordRealtimeRequest request)
            {
                ThrowIf.Null(request, "request");

                long   channelId = request.RequestContext.GetPrincipal().ChannelId;
                string staffId   = request.StaffId;

                // Get employee salt and password hash algorithm.
                GetEmployeePasswordCryptoInfoDataRequest employeePasswordCryptoInfoRequest = new GetEmployeePasswordCryptoInfoDataRequest(channelId, staffId);
                var    passwordCryptoInfo = request.RequestContext.Execute <SingleEntityDataServiceResponse <EmployeePasswordCryptoInfo> >(employeePasswordCryptoInfoRequest).Entity;
                string salt = passwordCryptoInfo.PasswordSalt ?? string.Empty;
                string passwordHashAlgorithm = passwordCryptoInfo.PasswordHashAlgorithm;

                if (string.IsNullOrEmpty(passwordHashAlgorithm))
                {
                    // get hash algorithm from the transaction service profile.
                    var getTransactionServiceProfileDataRequest         = new GetTransactionServiceProfileDataRequest();
                    TransactionServiceProfile transactionServiceProfile = request.RequestContext.Execute <SingleEntityDataServiceResponse <TransactionServiceProfile> >(
                        getTransactionServiceProfileDataRequest).Entity;
                    passwordHashAlgorithm = transactionServiceProfile.StaffPasswordHash;
                }

                // hash old password
                HashDataServiceRequest hashDataServiceRequest = new HashDataServiceRequest(request.OldPassword, passwordHashAlgorithm, request.StaffId, salt);
                string oldPasswordHash = request.RequestContext.Execute <HashDataServiceResponse>(hashDataServiceRequest).Data;

                // check if old password is correct
                EmployeeLogOnStoreDataRequest employeeDataRequest = new EmployeeLogOnStoreDataRequest(
                    channelId,
                    staffId,
                    oldPasswordHash,
                    new ColumnSet());

                if (request.RequestContext.Execute <SingleEntityDataServiceResponse <Employee> >(employeeDataRequest).Entity == null)
                {
                    // SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_InvalidPassword is used internally to identify when user provides incorrect password
                    // here we make sure this is not surfaced outside of the runtime, so there is no information disclosure
                    throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_InvalidAuthenticationCredentials, "Incorrect user name or password.");
                }

                // hash new password
                hashDataServiceRequest = new HashDataServiceRequest(request.NewPassword, passwordHashAlgorithm, request.StaffId, salt);
                string newPasswordHash = request.RequestContext.Execute <HashDataServiceResponse>(hashDataServiceRequest).Data;

                // return new password hash + salt
                return(new StaffChangePasswordRealtimeResponse(newPasswordHash, salt, passwordHashAlgorithm, DateTime.UtcNow, AuthenticationOperation.ChangePassword));
            }
            /// <summary>
            /// Logon the user on local store.
            /// </summary>
            /// <param name="request">The device activation request.</param>
            /// <param name="channelConfiguration">The channel configuration.</param>
            /// <returns>The device activation response.</returns>
            private static Employee EmployeeLogOnStore(StaffLogOnRealtimeRequest request, ChannelConfiguration channelConfiguration)
            {
                if (request == null)
                {
                    throw new ArgumentNullException("request");
                }

                Employee employee = null, localEmployee = null;
                string   passwordHash = string.Empty;
                string   staffId      = request.StaffId;

                if (request.ChannelId.HasValue && request.RequestContext.GetPrincipal().ChannelId == request.ChannelId.Value)
                {
                    // Get employee salt and password hash algorithm.
                    GetEmployeePasswordCryptoInfoDataRequest employeePasswordCryptoInfoRequest = new GetEmployeePasswordCryptoInfoDataRequest(request.ChannelId.GetValueOrDefault(), staffId);
                    var    passwordCryptoInfo = request.RequestContext.Execute <SingleEntityDataServiceResponse <EmployeePasswordCryptoInfo> >(employeePasswordCryptoInfoRequest).Entity;
                    string salt = passwordCryptoInfo.PasswordSalt ?? string.Empty;
                    string passwordHashAlgorithm = passwordCryptoInfo.PasswordHashAlgorithm;

                    if (!string.IsNullOrEmpty(request.StaffPassword))
                    {
                        if (string.IsNullOrEmpty(passwordHashAlgorithm))
                        {
                            // get hash algorithm from the transaction service profile.
                            var getTransactionServiceProfileDataRequest         = new GetTransactionServiceProfileDataRequest();
                            TransactionServiceProfile transactionServiceProfile = request.RequestContext.Execute <SingleEntityDataServiceResponse <TransactionServiceProfile> >(
                                getTransactionServiceProfileDataRequest).Entity;
                            passwordHashAlgorithm = transactionServiceProfile.StaffPasswordHash;
                        }

                        HashDataServiceRequest hashDataServiceRequest = new HashDataServiceRequest(request.StaffPassword, passwordHashAlgorithm, staffId, salt);
                        passwordHash = request.RequestContext.Execute <HashDataServiceResponse>(hashDataServiceRequest).Data;
                    }

                    // Logon to the store
                    EmployeeLogOnStoreDataRequest dataRequest = new EmployeeLogOnStoreDataRequest(
                        request.ChannelId.Value,
                        staffId,
                        passwordHash,
                        new ColumnSet());

                    localEmployee = request.RequestContext.Execute <SingleEntityDataServiceResponse <Employee> >(dataRequest).Entity;
                    if (localEmployee == null || localEmployee.Permissions == null)
                    {
                        throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthenticationFailed);
                    }

                    // Return new object for the logged-on employee with only the required fields to ensure not returning any sensitive data.
                    employee               = new Employee();
                    employee.Permissions   = new EmployeePermissions();
                    employee.Name          = localEmployee.Name;
                    employee.StaffId       = localEmployee.StaffId;
                    employee.NameOnReceipt = localEmployee.NameOnReceipt;
                    employee.Permissions   = localEmployee.Permissions;
                    employee.Images        = localEmployee.Images;
                    employee.PasswordLastChangedDateTime = localEmployee.PasswordLastChangedDateTime;

                    // Lock the user if multiple logins are not allowed.
                    if (!employee.Permissions.AllowMultipleLogins && channelConfiguration != null)
                    {
                        LockUserAtLogOnDataRequest lockDataRequest = new LockUserAtLogOnDataRequest(
                            request.ChannelId.Value,
                            request.RequestContext.GetTerminal().TerminalId,
                            staffId,
                            channelConfiguration.InventLocationDataAreaId);
                        bool locked = request.RequestContext.Execute <SingleEntityDataServiceResponse <bool> >(lockDataRequest).Entity;
                        if (!locked)
                        {
                            throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_UserLogonAnotherTerminal);
                        }
                    }
                }
                else
                {
                    RequestContext getEmployeeRequestContext;

                    // Employee record is associated with channel
                    // Decide what context is to be used to retrieve it
                    if (request.RequestContext.GetPrincipal().IsChannelAgnostic)
                    {
                        // If the context is channel agnostic (no channel information), then use current (first published channel in this case)
                        GetChannelIdServiceRequest  getChannelRequest  = new GetChannelIdServiceRequest();
                        GetChannelIdServiceResponse getChannelResponse = request.RequestContext.Execute <GetChannelIdServiceResponse>(getChannelRequest);

                        var anonymousIdentity = new CommerceIdentity()
                        {
                            ChannelId = getChannelResponse.ChannelId
                        };

                        getEmployeeRequestContext = new RequestContext(request.RequestContext.Runtime);
                        getEmployeeRequestContext.SetPrincipal(new CommercePrincipal(anonymousIdentity));
                    }
                    else
                    {
                        // If the request has channel information, then use current context
                        getEmployeeRequestContext = request.RequestContext;
                    }

                    GetEmployeeDataRequest dataRequest = new GetEmployeeDataRequest(staffId, QueryResultSettings.SingleRecord);
                    employee = getEmployeeRequestContext.Execute <SingleEntityDataServiceResponse <Employee> >(dataRequest).Entity;
                    if (employee == null)
                    {
                        string message = string.Format(CultureInfo.InvariantCulture, "The specified employee ({0}) was not found.", staffId);
                        throw new UserAuthenticationException(SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_AuthenticationFailed, message);
                    }

                    ICommercePrincipal principal = request.RequestContext.GetPrincipal();
                    RequestContext     contextForOperationAuthorization;
                    if (principal.IsAnonymous)
                    {
                        // create an authenticated context that can be used for specific operation authorization
                        RequestContext   authenticatedContext = new RequestContext(request.RequestContext.Runtime);
                        CommerceIdentity identity             = new CommerceIdentity(
                            employee,
                            new Device()
                        {
                            DeviceNumber     = principal.DeviceNumber,
                            Token            = principal.DeviceToken,
                            ChannelId        = principal.ChannelId,
                            TerminalRecordId = principal.TerminalId
                        });
                        authenticatedContext.SetPrincipal(new CommercePrincipal(identity));

                        contextForOperationAuthorization = authenticatedContext;
                    }
                    else
                    {
                        contextForOperationAuthorization = request.RequestContext;
                    }

                    GetEmployeePermissionsDataRequest permissionsDataRequest = new GetEmployeePermissionsDataRequest(staffId, new ColumnSet());
                    employee.Permissions = contextForOperationAuthorization.Execute <SingleEntityDataServiceResponse <EmployeePermissions> >(permissionsDataRequest).Entity;
                }

                return(employee);
            }
Example #5
0
            /// <summary>
            /// Validate Device token.
            /// </summary>
            /// <param name="deviceId">The device identifier.</param>
            /// <param name="deviceTokenData">The device token data.</param>
            /// <param name="deviceToken">The device token.</param>
            /// <param name="requestContext">Request context.</param>
            /// <returns>Device response.</returns>
            private static Device ValidateDeviceTokenLocally(string deviceId, string deviceTokenData, string deviceToken, RequestContext requestContext)
            {
                var getTransactionServiceProfileDataRequest         = new GetTransactionServiceProfileDataRequest();
                TransactionServiceProfile transactionServiceProfile = requestContext.Runtime.Execute <SingleEntityDataServiceResponse <TransactionServiceProfile> >(getTransactionServiceProfileDataRequest, requestContext).Entity;

                int deviceTokenExpirationInDays = transactionServiceProfile.DeviceTokenExpirationInDays;

                // only validate device if connecting against master database
                bool mustValidateActiveDevice = requestContext.Runtime.Configuration.IsMasterDatabaseConnectionString;

                // Get the device
                GetDeviceDataRequest getDeviceRequest = new GetDeviceDataRequest(deviceId, mustValidateActiveDevice);
                Device localDevice = requestContext.Execute <SingleEntityDataServiceResponse <Device> >(getDeviceRequest).Entity;

                if (localDevice == null)
                {
                    throw new DeviceAuthenticationException(
                              SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_LocalDeviceAuthenticationFailed,
                              string.Format("Device not found for device '{0}'.", deviceId));
                }

                localDevice.Token = deviceToken;

                string algorithm = localDevice.TokenAlgorithm;

                if (string.IsNullOrEmpty(algorithm))
                {
                    // string algorithm = transactionServiceProfile.DeviceTokenAlgorithm;
                    algorithm = "SHA256";
                }

                if (mustValidateActiveDevice)
                {
                    if (string.IsNullOrEmpty(localDevice.TokenSalt))
                    {
                        throw new DeviceAuthenticationException(
                                  SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_LocalDeviceAuthenticationFailed,
                                  string.Format("TokenSalt not available for device '{0}'.", deviceId));
                    }

                    if (localDevice.ActivationStatus != DeviceActivationStatus.Activated)
                    {
                        throw new DeviceAuthenticationException(
                                  SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_LocalDeviceAuthenticationFailed,
                                  string.Format("Device is not activated, actual state is '{0}'.", localDevice.ActivationStatus));
                    }

                    // Get hashed value for device token
                    HashDataServiceRequest hashDataServiceRequest = new HashDataServiceRequest(deviceTokenData, algorithm, localDevice.DeviceNumber, localDevice.TokenSalt);
                    string deviceTokenHash = requestContext.Execute <HashDataServiceResponse>(hashDataServiceRequest).Data;

                    // Validate the hashed device token with value in the database
                    if (!string.Equals(deviceTokenHash, localDevice.TokenData))
                    {
                        throw new DeviceAuthenticationException(
                                  SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_LocalDeviceAuthenticationFailed,
                                  string.Format("Device token for '{0}' record was not found.", deviceId));
                    }

                    if (!localDevice.TokenIssueTime.HasValue)
                    {
                        throw new ConfigurationException(
                                  ConfigurationErrors.Microsoft_Dynamics_Commerce_Runtime_ActivatedDeviceMissingTokenIssueDatetime,
                                  string.Format("Activated device '{0}' does not have an associated token issue datetime.", deviceId));
                    }

                    if (DateTimeOffset.Compare(DateTimeOffset.Now, localDevice.TokenIssueTime.Value.AddDays(deviceTokenExpirationInDays)) > 0)
                    {
                        throw new DeviceAuthenticationException(
                                  SecurityErrors.Microsoft_Dynamics_Commerce_Runtime_DeviceTokenExpired,
                                  string.Format("Device Token for device '{0}' expired.", deviceId));
                    }
                }
                else
                {
                    // when device comes from non-master database, it might have stale data
                    localDevice = ConstructDeviceFromToken(deviceToken);
                }

                return(localDevice);
            }