Sep 17, 2020
UPDATED: Oct 25, 2024
Implementing additional layers of security can protect against common vulnerabilities, such as cross-site request forgery (CSRF) when using Optimizely Forms.
A critical defense mechanism against this threat is the AntiForgeryToken, which helps verify that form submissions are authentic and initiated by trusted users. In this blog, we’ll explore how to implement these tokens manually.
The Common Problem
When displaying a form, it's crucial to protect it from malicious attacks. In .NET Core, this can typically be achieved by using the [validateantiforgerytoken] attribute on the Action method and invoking @Html.AntiForgeryToken() in the corresponding view.
However, certain scenarios may prevent this standard approach from working. One such case occurs when a webhook sends data to the backend. In these instances, Optimizely's (formerly Episerver) context can override the existing context, potentially discarding important data required to validate the view model. As a result, a custom solution is needed to validate the posted data manually.
The Solution
To address the challenge of validating form submissions in scenarios where standard anti-forgery measures are inadequate— such as when using webhooks that bypass the usual request pipeline— it's necessary to implement a custom validation mechanism. A robust approach involves generating a hidden token within the form, which is then transmitted alongside the form data. This token serves as a unique identifier for the request, allowing the server to verify the authenticity of the submission. By embedding this token, you ensure that only forms originating from your application are processed, thereby mitigating the risk of cross-site request forgery (CSRF) attacks.
The first step is to create a block that will store the hidden token necessary for validation. This token will be generated and included in the form to ensure secure validation during data submission.
AntiforgeryTokenElementBlock
[ContentType(
DisplayName = "AntiForgeryToken", GUID = "{DD088FD8-895E-47EF-9497-5B7A6700F4A6}",
GroupName = EPiServer.Forms.Constants.FormElementGroup_Container,
Order = 4000)]
public class AntiforgeryTokenElementBlock : ElementBlockBase
{
public virtual string AntiForgeryToken { get; set; }
public override void SetDefaultValues(ContentType contentType)
{
base.SetDefaultValues(contentType);
}
}
To maintain an organized and loosely coupled architecture, it’s best to delegate content creation to a factory that can be easily registered as a dependency in Optimizely. This factory will leverage additional components, such as an encryption handler and a storage handler. Optimizely offers simple implementations for these tasks. This approach takes a GUID as a string and encodes it using two separate seeds. One of the encoded strings will be rendered in a hidden form field. At the same time, the second must be securely stored, either using the IStateStorage interface or a custom storage solution, such as a database or a caching service like Redis. This ensures secure and efficient handling of form data.
AntiForgeryFactory
public class AntiForgeryFactory : IAntiForgeryFactory
{
private const string SessionGuidSeed = "70e83b85-9135-42f1-a48d-4e1c5349b412";
private const string FieldGuidSeed = "0b108811-3e25-40fb-8f97-403012b0f618";
private readonly EncryptionHandler _sessionEncryptDecrypt;
private readonly EncryptionHandler _fieldEncryptDecrypt;
private const string TokenName = "_tokensession";
private readonly IStateStorage _storage;
public AntiForgeryFactory(IStateStorage storage)
{
_storage = storage;
_fieldEncryptDecrypt = new EncryptionHandler(FieldGuidSeed);
_sessionEncryptDecrypt = new EncryptionHandler(SessionGuidSeed);
}
public string CreateToken(string token)
{
var field = _fieldEncryptDecrypt.Encrypt(token);
var session = _sessionEncryptDecrypt.Encrypt(token);
_storage.Save($"{TokenName}:{token}", session);
return field;
}
public bool CheckToken(string field)
{
var fieldToken = _fieldEncryptDecrypt.Decrypt(field);
var key = $"{TokenName}:{fieldToken}";
var session = _storage.Load(key).ToString();
if (session == string.Empty) return false;
_storage.Delete(key);
var sessionToken = _sessionEncryptDecrypt.Decrypt(session);
return sessionToken.Equals(fieldToken);
}
}
InMemoryStateStorage
[InitializableModule]
internal class InMemoryStateStorage : IStateStorage, IConfigurableModule
{
IDictionary<string,> _states = new Dictionary<string,>();
public bool IsAvailable => true;
public object Load(string key)
{
_states.TryGetValue(key, out var value);
return value ?? string.Empty;
}
public void Save(string key, object value)
{
_states[key] = (string)value;
}
public void Delete(string key)
{
_states.Remove(key);
}
public void ConfigureContainer(ServiceConfigurationContext context)
{
context.Services.Add((s) => new VisitorGroupOptions(), ServiceInstanceScope.Singleton);
context.Services.Configure(s => s.EnableSession = false);
context.Services.Add(s => new InMemoryStateStorage(), ServiceInstanceScope.Singleton);
}
public void Initialize(InitializationEngine context) { }
public void Uninitialize(InitializationEngine context) { }
}
</string,></string,>
Since the token must be renewed each time the form is loaded, a controller is required to assign the previously mentioned GUID. This controller will generate a new token for each form request, ensuring that each submission is uniquely validated. By assigning a fresh GUID with every form load, you ensure a higher level of security, preventing replay attacks and safeguarding the integrity of the form submission process. This approach helps maintain the dynamic nature of token generation while keeping the validation mechanism secure and efficient.
AntiforgeryTokenElementBlockController
public class AntiforgeryTokenElementBlockController : PartialContentComponnet
{
public static string ViewPath = "~/Views/Forms/ElementBlocks/AntiforgeryTokenElementBlock.cshtml";
private readonly IAntiForgeryFactory _antiForgery;
public AntiforgeryTokenElementBlockController(IAntiForgeryFactory antiForgery)
{
_antiForgery = antiForgery;
}
public override IViewComponentResult InvokeComponent(AntiforgeryTokenElementBlock currentBlock)
{
var model = currentBlock.CreateWritableClone() as AntiforgeryTokenElementBlock ?? new AntiforgeryTokenElementBlock();
var field = _antiForgery.CreateToken(Guid.NewGuid().ToString());
model.AntiForgeryToken = field;
return View(ViewPath, model);
}
}
Next, the encrypted token must be rendered in a hidden field, which can be efficiently accomplished by utilizing the previously created block.
AntiForgeryTokenElementBlock.cshtml
@using EPiServer.Forms.Helpers.Internal
@model AlloyTraining.Models.ElementBlocks.AntiforgeryTokenElementBlock
@{
var formElement = Model.FormElement;
}
@using (Html.BeginElement(Model, new { }))
{
}
With the custom block created and the token generation and rendering in place, the next step is constructing the form. The form will require only a few elements: the custom block (accessible under "Form Element Blocks" on the left side) and a submit button.
We will also need to enter the Webhook URL of the listening endpoint. In this case:
http://localhost:52271/Forms/NewsFeedSubscription
With the webhook triggering a POST request to the specified URL, the next step is configuring the corresponding endpoint on the Optimizely backend. It is common practice to name this endpoint FormsController for clarity and consistency.
FormsController
public class FormsController : Controller
{
private readonly IAntiForgeryFactory _antiForgery;
private readonly ILogger _logger;
public FormsController(IAntiForgeryFactory antiForgery, ILogger logger)
{
_antiForgery = antiForgery;
_logger = logger;
}
[HttpPost]
public JsonResult NewsFeedSubscription(NewsFeedSubscriptionRequestVM model)
{
var valid = _antiForgery.CheckToken(model.AntiforgeryTokenElementBlock);
_logger.Error($"Anti forgery validation for NewsFeedSubscription: {valid}");
return new JsonResult();
}
}
This means the model you're using should align with the Name attribute defined during the field creation. This alignment is crucial because the system will map the data based on this attribute. Essentially, if the model's attribute names or structure don’t match what's expected, it could lead to issues with how data is processed or accessed.
NewsFeedSubscriptionRequestVM
public class NewsFeedSubscriptionRequestVM : AntiForgeryTokenVM
{
public string Email { get; set; }
public string FullName { get; set; }
}
We should use a well-defined interface to access the token property to maintain loose coupling in our code. This approach ensures that we don’t interfere with the business logic defined in our view model, allowing for better separation of concerns and easier maintenance.
AntiForgeryTokenVM
public class AntiForgeryTokenVM
{
public string AntiforgeryTokenElementBlock { get; set; }
}
However, to reach this endpoint, we must register the default route in our StartUp.cs file like this:
StartUp.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: “Default”,
pattern: “{controller}/{action}”);
endpoints.MapControllers();
}
}
We can check if the form has rendered our hidden field with the encrypted token as the field value.
Now that everything is set up, we can test our form.
As anticipated, our breakpoint was triggered with the data sent via the form and the webhook.
Now, we can decode the field string back into a GUID, which we’ll use to retrieve and decode the previously stored second encoded string. Comparing both GUIDs should result in a match, confirming validation and allowing only one submission per token.
We could create a decorator to manage the validation:
CustomAntiForgeryTokenValidation
public class CustomAntiForgeryTokenValidation : ActionFilterAttribute
{
private readonly IAntiForgeryFactory _antiForgery;
public CustomAntiForgeryTokenValidation()
{
_antiForgery = ServiceLocator.Current.GetInstance();
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var token = filterContext.ActionParameters["model"] as AntiForgeryTokenVM;
var result = _antiForgery.CheckToken(token.AntiforgeryTokenElementBlock);
if (!result)
{
filterContext.Result = new JsonResult { Data = new { Message = "Error validating the token", Result = false } };
}
}
}
This approach allows us to submit and handle additional data within the decorator rather than at the endpoint. Given the token's validity, the subsequent breakpoint will be at the endpoint. At this point, we can proceed with removing the antiforgery validation.
However, if we refresh the browser page and resubmit the form, the submission will not be validated, and the call to the endpoint will not be executed. However, if we refresh the browser page and resubmit the form, the submission will not be validated, and the call to the endpoint will not be executed.
However, if we refresh the browser page and resubmit the form, the submission will not be validated, and the call to the endpoint will not be executed.
Wrapping Up
Optimizely Forms is a versatile tool for building web forms used in a variety of applications, such as event registrations, job applications, and customer surveys. It allows you to collect and store submitted data, which can be exported in multiple formats for further analysis or use. However, it’s also important to ensure the security of these forms.
Oshyn is an Optimizely partner with the required expertise to ensure the security of your implementation and the best customer experience. With consistent refinements and improvements, you can keep your website fresh and responsive to customer expectations and your business goals. Check out our Enhance & Maintain service for a painless way to realize these benefits. Contact us to learn more.
Related Insights
-
Oshyn
-
Oshyn
Optimizely CMS 12
Tips for a Successful Upgrade
-
-
Oshyn
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.