I’m using the latest Sitecore Reference Storefront available from Github for this walk through: https://github.com/Sitecore/Reference-Storefront/releases/tag/8.1.478.2
Sitecore Commerce Server supports asymmetric encryption and one-way hashing to enable the encryption of profile properties.
One-way hashing is used typically for passwords. Once a string is hashed, we should not be able to get back to the original string (theoretically). So what is done instead, when a user enters their password, we hash the entered password and compare it to the hashed password saved to the database. A once common hashing algorithm – MD5 has been proven to be breakable via a simple google search, so use a more secure hashing algorithm.
Asymmetric encryption is used for user’s personal details like phone numbers, emails, etc. It’s used in cases where you need to ensure, if your database is compromised (god forbid), user data is not available in plaintext.
You also want keep in mind that encrypted data is not searchable and not to mark properties that have preexisting data as encryptable. Otherwise, the preexisting data will be lost.
This post will focus on adding an asymmetric encrypted property.
1.0 – Sitecore Commerce Server Setup
1.1 – Add database field.
- Click the Windows Start button and type Management Studio and select SQL Server Management Studio.
- Navigate to the %SiteName%_profiles database.
- Right click on UserObject table and select Design.
- Add a new column with these details:
Column Name Data Type Allow Nulls u_skype_name nvarchar(512) Tick NOTE: If you are unable to save changes edit SSMS options Designers –> Table and Database Designer. Untick Prevent saving changes that require table re-creation.
- Save.
1.2 – Add property to Data Source in Commerce Server Manager
- Click the Windows Start button and type commerce and then select the Commerce Server Manager.
- Expand Commerce Server Manager, expand Global Resources, expand Profile Catalog, expand Data Sources, expand ProfileService_SQLSource, expand Data Objects, and right click User Object and select New Data Member.
- Enter these Details:
Member Name: u_skype_name Display Name: Skype Name Description: Skype Name Data Type: String - Click Add, and then click Finish.
1.3 – Add property to the Profile Definition in Commerce Server Manager
- Click the Windows Start button and type commerce and then select the Commerce Server Manager
- Expand Commerce Server Manager, expand Global Resources, expand Profile Catalog, expand Profile Definitions, and then click User Object.
- Expand the property group you wish to add to. In this case I have created a new property group called Lab Info.
- Click Add, and select Add a new property.
- In the Attributes section, enter the following:
Name skype_name Display Name Skype Name Description Skype Name Type String Type Reference Select as blank. Cannot be a JoinKey, PrimaryKey, DualKey, or HashingKey. - Click the Advanced Attributes section, and enter the following:
Active Check Yes. Map to Data Click ellipsis button […] to navigate to the SkypeName property we created in the Profile Data Source from the previous step. Key type Leave blank. The key type cannot be a UniqueKey, JoinKey, PrimaryKey, DualKey, or HashingKey.
Encrypted Asymmetric encryption Exported Clear the Yes check box. Multi-valued Leave blank. Searchable Clear the Yes check box. Hashing Key Clear the Yes check box. - Click the Custom Attributes section, and enter the following:
Name Display name Description Value IsRestricted Is Property Restricted Indicates that this property requires an elevated priviledge level in order to perform an update. 0 MaxLength Maximum Length Maximum length of this property value 4000 MinLength Minimum Length Minimum length of this property value 0 - Click Apply, and Save on the top ribbon.
1.4 – Enable property in the Management Webservices
- Navigate to where the profile webservices e.g. C:HostscsDemoServicesCSSolutionStorefrontsite_ProfilesWebService
- Open the web.config file and ensure the public and private encryption keys are specified. The public and private keys settings need to match to the ones specified in the website
- You can optionally add the fields details to one of the language files to override the display name e.g. en_PresentationInfo.xml.
1.5 – Recycle app pool
- Click the Windows Start button and type IIS and then select the Internet Information Services (IIS) Manager.
- Navigate in the left pane and click on Application pools.
- In the right pane, right click on the csDemo (the app pool being used by the website) and select Recycle.
- In the right pane, right click on the csDemoServices (the app pool being used by the web services) and select Recycle.
1.6 – Test
- Click the Windows Start button and type commerce and then select the Commerce Server Customer and Orders Manager.
- Click Profiles, from the Look in: dropdown select User Object, from the Look for: dropdown select E-mail, blank out the Keyword: field and click Find.
- Double click on a search result and go to the Advanced tab, and scroll down until you find the Skype Name field.
- Enter demo@sitecore.net into the field and save.
NOTE: you will need to set a number of other mandatory fields before you can save from the Customer and Orders Manager. - Verify in SSMS – note the encrypted field u_skype_name
2.0 – Sitecore Setup
2.1 – Changes to web.config
- Click the Windows Start button and type studio and then select the Visual Studio 2015.
- Click File, select Open, select Project and Solutions, and open:
Reference-StorefrontStorefrontCommerce.Server.Storefront.sln - In the Commerce.Storefront project drill down to: web.config
- Search for:
</properties>
and add:
<add name="skype_name" type="System.String" customproviderdata="cs|LabInfo.skype_name" /> </properties> </profile>
- Deploy the solution.
2.2 – Add the property to the Commerce User Security Data Template
- Log into Sitecore and click the Desktop icon.
- Click the Database Selector icon and select core.
- Open the Content Editor, navigate to sitecore > templates > System > Security and select Commerce User.
- Click the Builder tab and add a new field skype_name at the end of the Commerce section. Now check the checkbox Shared and then click Save.
- Click the Database Selector icon and select master.
2.3 – Test
- Log into Sitecore and click the User Manager icon.
- Select an account in the CommerceUsers domain and select Edit.
- Click the Profile tab and click the Edit button.
- Scroll down to the skype_name field, enter ChangedInUserManager and click OK.
- Click OK again.
- Open the Customer and Orders Manager and check the field has been changed.
3.0 – Reference Storefront Setup
Before starting with the details I want to mention there are two approaches that can be taken when incorporating these changes into the Storefront. One is to use the property bag as I am doing below and the other is to inherit from CommerceUser and extent with strongly typed member fields. I’ve chosen the property bag approach purely because it aligns with the current SCpbCS training course.
3.1 – Model Changes
- Click the Windows Start button and type studio and then select the Visual Studio 2015.
- Click File, select Open, select Project and Solutions, and open:
Reference-StorefrontStorefrontCommerce.Server.Storefront.sln - In the Common project open file:ModelsProfileModel.cs
- Search for:
public string TelephoneNumber { get; set; }
and merge:
/// /// Gets or sets the telephone /// [Required] [Display(Name = "Telephone")] public string TelephoneNumber { get; set; } /// /// Gets or sets the skype name /// [Required] [Display(Name = "Skype Name")] public string SkypeName { get; set; }
3.2 – Page Changes
- In the CommonSettings project open file:ViewsStorefrontAccountEditProfile.cshtml
- Search for:
@Html.ValidationMessageFor(m => m.TelephoneNumber)
and merge:
<div> <div class="form-group"> @telephoneNumber<span class="required">*</span> @Html.TextBoxFor(m => m.TelephoneNumber, new { placeholder = @telephoneNumberPH, data_val_required = @telephoneRequiredMsg, @class = "form-control" }) @Html.ValidationMessageFor(m => m.TelephoneNumber) </div> <div class="form-group"> Skype Name @Html.TextBoxFor(m => m.SkypeName, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.SkypeName) </div> </div>
3.3 – Controller Changes
- In the Commerce.Storefront project open file:ControllersAccountController.cs
- Search for:
model.TelephoneNumber = commerceUser.GetPropertyValue("Phone") as string;
and merge:
model.TelephoneNumber = commerceUser.GetPropertyValue("Phone") as string; model.SkypeName = commerceUser.GetPropertyValue("skype_name") as string; return View(this.GetRenderingView("EditProfile"), model);
- Search again for:
model.TelephoneNumber = commerceUser.GetPropertyValue("Phone") as string;
and merge:
model.TelephoneNumber = commerceUser.GetPropertyValue("Phone") as string; model.SkypeName = commerceUser.GetPropertyValue("skype_name") as string; } } Item item = Context.Item.Children.SingleOrDefault(p => p.Name == "EditProfile");
3.4 – Manager Changes
- In the Commerce.Storefront project open file:ManagersAccountManager.cs
- Search for:
model.TelephoneNumber = commerceUser.GetPropertyValue("Phone") as string;
and merge:
model.TelephoneNumber = commerceUser.GetPropertyValue("Phone") as string; model.SkypeName = commerceUser.GetPropertyValue("skype_name") as string; return View(this.GetRenderingView("EditProfile"), model);
- Search for:
commerceUser.SetPropertyValue("Phone", inputModel.TelephoneNumber);
and merge:
public virtual ManagerResponse<UpdateUserResult, CommerceUser> UpdateUser([NotNull] CommerceStorefront storefront, [NotNull] VisitorContext visitorContext, ProfileModel inputModel) { Assert.ArgumentNotNull(storefront, "storefront"); Assert.ArgumentNotNull(visitorContext, "visitorContext"); Assert.ArgumentNotNull(inputModel, "inputModel"); UpdateUserResult result; var userName = visitorContext.UserName; var commerceUser = this.GetUser(userName).Result; if (commerceUser != null) { commerceUser.FirstName = inputModel.FirstName; commerceUser.LastName = inputModel.LastName; commerceUser.Email = inputModel.Email; commerceUser.SetPropertyValue("Phone", inputModel.TelephoneNumber); commerceUser.SetPropertyValue("skype_name", inputModel.SkypeName);
3.5 – Connect Pipeline Changes
- In the Commerce.Storefront project open file:ConnectPipelinesCustomersGetUser.cs
- Search for:
commerceUser.ExternalId = userProfile["user_id"];
and merge:
protected void UpdateCustomer(Sitecore.Commerce.Entities.Customers.CommerceUser commerceUser, Sitecore.Security.UserProfile userProfile) { commerceUser.ExternalId = userProfile["user_id"]; commerceUser.SetPropertyValue("skype_name", userProfile["skype_name"]); Assert.IsNotNullOrEmpty(commerceUser.ExternalId, "commerceUser.ExternalId"); if (commerceUser.Customers == null || commerceUser.Customers.Count == 0) { var customers = new List<string>() { commerceUser.ExternalId }; commerceUser.Customers = customers.AsReadOnly(); } }
- In the Commerce.Storefront project create a file:ConnectPipelinesCustomersUpdateUser.cs
- Paste this into the file:
namespace Sitecore.Reference.Storefront.Connect.Pipelines.Customers { using Sitecore.Commerce.Data.Customers; using Sitecore.Commerce.Pipelines.Customers.UpdateUser; using Sitecore.Commerce.Services.Customers; using Sitecore.Diagnostics; using Sitecore.Security; using Sitecore.Security.Accounts; using System; using System.Collections.Generic; using System.Linq; using System.Web; public class UpdateUser : UpdateUserInSitecore { public UpdateUser(IUserRepository userRepository) : base(userRepository) { } public override void Process(Commerce.Pipelines.ServicePipelineArgs args) { var request = (UpdateUserRequest)args.Request; if (request.CommerceUser == null) { return; } // if we found a user, add some addition info var userProfile = GetUserProfile(request.CommerceUser.UserName); Assert.IsNotNull(userProfile, "profile"); UpdateCustomer(request.CommerceUser, userProfile); base.Process(args); } protected void UpdateCustomer(Sitecore.Commerce.Entities.Customers.CommerceUser commerceUser, Sitecore.Security.UserProfile userProfile) { userProfile["skype_name"] = commerceUser.GetPropertyValue("skype_name").ToString(); userProfile.Save(); } protected UserProfile GetUserProfile(string userName) { return User.FromName(userName, true).Profile; } } }
- In the Commerce.Storefront project open file:App_ConfigIncludeReference.StorefrontReference.Storefront.Customer.config
- Search for:
</commerce.customers.getUser>
and merge:
<pipelines> <commerce.customers.getUser> <processor type="Sitecore.Commerce.Pipelines.Customers.GetUser.GetUserFromSitecore, Sitecore.Commerce"> <patch:attribute name="type">Sitecore.Reference.Storefront.Connect.Pipelines.GetUser, Sitecore.Reference.Storefront.Powered.by.CommerceServer</patch:attribute> </processor> </commerce.customers.getUser> <commerce.customers.updateUser> <processor type="Sitecore.Commerce.Pipelines.Customers.UpdateUser.UpdateUserInExternalSystem, Sitecore.Commerce"> <patch:delete/> </processor> <processor type="Sitecore.Commerce.Pipelines.Customers.UpdateUser.UpdateUserInSitecore, Sitecore.Commerce"> <patch:attribute name="type">Sitecore.Reference.Storefront.Connect.Pipelines.Customers.UpdateUser, Sitecore.Reference.Storefront.Powered.by.CommerceServer</patch:attribute> </processor> </commerce.customers.updateUser>
- Deploy the site.
3.6 – Test