Have you ever wanted to share content with your subscribers, without exposing it to anyone – even when available on a public URL? As you know, there are already methods of securing Cloud Pages, especially if using Cloud Pages as apps. But this limits us to only showing the page to logged in users in SFMC.
One-time password explained
Are there any other ways of ensuring not everyone can follow a link, in case an email has been forwarded, and access the linked content? Well – we can build a solution which will utilise a one-time password (OTP). But what is an OTP?
A one-time password is a security feature used to authenticate users. Unlike traditional passwords, OTPs are valid for only a single login session or transaction, providing an extra layer of security. Generated either randomly or using a predefined algorithm, OTPs are typically delivered to the user via SMS, email, or an authentication app.
The primary advantage of OTPs is their resistance to replay attacks, where an attacker intercepts and reuses a valid password. Because an OTP changes with each use, even if an attacker intercepts it, the OTP cannot be reused. This method helps in securing sensitive operations like online banking, access to secure areas, or confirming identity during transactions.
By being unique and temporary, OTPs enhance security significantly, reducing the risks associated with stolen, guessed, or reused passwords.
Below diagram shows how we will be building the OTP solution:
Building blocks required
- Email containing the link to secured Cloud Page
- Cloud Page handling OTP generation, sendout and validation
- Data extension for storing OTP details
- Transactional Send journey
- Email containing the OTP
Let’s take these on one by one.
Email containing the link to secured Cloud Page
Imagine you want to send an email to your subscribers, where they need to access a cloud page, but prevent this cloud page from being viewed by others – even if the email gets forwarded. To achieve this, we will use CloudPagesUrl function, but not pointing it directly to this cloud page, but point it to our “OTP gate”. The code here is very simple:
<a href="%%=RedirectTo(CloudPagesUrl(9999,'cpurl','1234'))=%%" alias="Here we go" target="_blank">Show me something secret</a>
As you can see, we are using two different cloud page IDs: 9999 and 1234.
If you can’t remember where to find the ID of a Cloud Page, you can easily do this by going into Page Properties. As you can see, the Cloud Page ID is displayed as a number on the very bottom of the properties.
We will need two different numbers, one for our destination cloud page – this one is the page we want to secure, preventing it from being accessed by anyone with the link. This will be the second parameter in the link in our email. 1234 in our example above. How this will be utilised, we will get back to later.
The first, and “real” cloud page ID (9999 in our example above) is linking to the cloud page with our security logic. This page will receive the click from the email, and facilitate the entire authentication process. As per standard CloudPagesUrl functionality, parameters such as email and subscriber key are passed on automatically, so we don’t need to declare them explicitly like we do with the cpurl
parameter.
We will need the subscriber key and email address later on, as we will trigger the transactional send to the recipient, providing the OTP.
Cloud Page handling OTP generation, sendout and validation
This is where all the heavy lifting happens. You can find the complete code in the gist below. It has some comments, so it should be self-explanatory:
From a user perspective, this is what happens:
When you click on the link in your email, you are directed to this page, seeing four digit input form. When you are seeing this, it means that the page has automatically picked up your email address and subscriber key, and proceeded to process the OTP generation.
- OTP is a random 4-digit number between 1000 and 9999
- We are also creating a GUID, which becomes our unique identifier of this specific OTP generation
- Current timestamp is recorded, showing the time of OTP generation
- An expiry date is set, which is done by adding 10 minutes to the above timestamp
- Subscriber key and email address of the recipient are stored
- A field called invalidated is automatically set to false.
This data is now becoming the basis of our access validation, and is sent in a new email (using Transactional Messaging API) to the person accessing the OTP page.
I recommend setting a 24 hour data retention in the data extension, ensuring we don’t store old OTPs.
Using the Transactional Messaging API ensures the email is sent within few seconds of the OTP page access. You don’t need any fancy code in the email, so it might even be a very minimal text-email, containing nothing more than: Your one time code is: %%=AttributeValue("OTP")=%%
Now, you can take the code from the email, and type it into the form:
Once you click Verify we are submitting the code, along with the guid (saved in hidden form field, along with few other details). Upon submission, the OTP code is being verified against the row in the MFA data extension with the corresponding GUID.
We are also checking whether the code has been used before the expiry date (less than 10 minutes after generation), and it is not invalidated. We are setting the Invalidated field to TRUE once the validation has been successful, ensuring same code cannot be used more than once. It would most probably not even be an issue, as we need not only the code, but also the combination the code and the guid in the hidden field. But we are securing this as good as we can.
Once all this is done, you COULD consider redirecting the visitor to the destination URL, by using Redirect function, like this: %%=Redirect(CloudPagesUrl(Field(@accessRow,"pageID")))=%%
This would work nicely, only redirecting if you have provided the correct OTP. But the downside is, you will be able to see the destination URL of your “secured” Cloud Page and share it with anyone. These people would not need to use our verification, as the OTP step will be entirely bypassed.
Instead we are doing something more radical. We are fetching the entire HTML of the secured page, using HTTPGet function:
%%[
var @getRequest
set @getRequest = TreatAsContent(HTTPGet(CloudPagesUrl(Field(@accessRow,"pageID"))))
]%%
%%=v(@getRequest)=%%
This will take the Cloud Page ID from our MFA data extension, fetch the HTML and display it. The drawback of this solution is the lack of sending any details of the recipient to the secured Cloud Page. However it can be solved adding e.g. subscriber key as an additional parameter to CloudPagesUrl function wrapped in HTTPGet.