Securing Cloudpages in Salesforce Marketing Cloud

Securing Cloudpages in Salesforce Marketing Cloud

In this article I will show a few ways to secure a Cloudpage from external access using AMPScript and SSJS. This can be seen as an addition to my last post about checking if a user is logged in to Marketing Cloud. I will touch on this solution again but add two more. I also recommend reading the popular post by Markus Slabina about securing Cloudpages.

With my article I want to go in a slightly different direction. The following code examples show how you can limit the Cloudpage to be only displayed to particular user groups or users for whom the content or functionality is intended. Therefore I will talk about these three features:

  1. IP Address Whitelisting
  2. Check Referrer URL
  3. Check User login in SFMC

IP Address Whitelisting

IP address whitelisting is an easy way to limit the cloudpage access to your company’s IP addresses. While the IP addresses of private households change from time to time, companies often have fixed IP addresses or IP address ranges. So you can use this network address to ensure the user is coming from the company’s network. At this point I started with the code from Markus

<script runat="server">
    Platform.Load("Core", "1");

    var allowedIPs = [
        "xxx.xxx.xxx.xxx"
    ];

    try {
        var validIp = false;
        for (var i = 0; i < allowedIPs.length; i++) {
            if (allowedIPs[i] == Platform.Request.ClientIP) {
                validIp = true;
            }
        }

        if (validIp) {
</script>

INSERT YOUR REGULAR CLOUDPAGE CODE HERE

<script runat="server">
        } else {
            // Request originates from wrong IP
            throw new Error("Non-whitelisted IP");
        }
    } catch (e) {
    	// You could also redirect to an error page here.
    	// This catch-block catches any error within the try-block, so not only wrong IP addresses
        Write(Stringify(e));
    }
</script>

In my case we have IP ranges that differ in their last part. To cover more than single addresses and to save me from writing a long list, I adapted the code a bit. Now it cuts every address in its parts and checks if the user’s IP address is in this range. Therefore the IP addresses need to be written in the following pattern: Number.Number.Number/FromNumber-ToNumber

As an example the code for the IP range 192.0.1.1 to 192.0.1.254 would be:
192.0.1/1-254

If you want to have a single address whitelisted you can still write it this way:
192.0.1/1-1

To cut the IP addresses in the right parts, I use the methods substring, indexOf and lastIndexOf. Then I just need to compare the values.

Now you can use it in the adapted code:

<!-- Start IP Whitelist -->
 
<script runat="server">
    Platform.Load("Core", "1");
 
    var allowedIPs = [
      '192.0.1/1-254', //IP range 1
      '172.217.30/9-9' //IP range 2
       
    ];
 
   var clientIP = Platform.Request.ClientIP;
   var clientIP_part_1 = clientIP.substring(0,clientIP.lastIndexOf('.'));
   var clientIP_part_2 = clientIP.substring(clientIP.lastIndexOf('.')+1,15);
 
   
    try {
        var validIp = false;
        for (var i = 0; i < allowedIPs.length; i++) {
           var ip_part_1 = allowedIPs[i].substring(0, allowedIPs[i].indexOf('/'));
           var ip_part_start = allowedIPs[i].substring(allowedIPs[i].indexOf('/')+1, allowedIPs[i].indexOf('-'));
           var ip_part_end = allowedIPs[i].substring(allowedIPs[i].indexOf('-')+1,20);
      
           
          if (ip_part_1 == clientIP_part_1 && ip_part_start <= clientIP_part_2 && ip_part_end >= clientIP_part_2) {
                validIp = true;
            }
        }
 
        if (validIp) {
</script>

CONTENT

<!-- END OF IP WHITELIST -->
<script runat="server">
 
 
        } else {
            // Request originates from wrong IP
            throw new Error("Non-whitelisted IP");
        }
    } catch (e) {
     // You could also redirect to an error page here.
     // This catch-block catches any error within the try-block, so not only wrong IP addresses
        Write(Stringify(e));
    }
</script>

In addition I am not a fan of placing the script around the rest of my content. I decided to make a redirect to Google if the user’s IP address doesn’t match my criteria. So I can place the whole script at the beginning of my page before the actual content.

<script runat="server">
    Platform.Load("Core", "1");
 
    var allowedIPs = [
      '192.0.1/1-254', //IP range 1
      '172.217.30/9-9' //IP range 2
       
    ];
 
   var clientIP = Platform.Request.ClientIP;
   var clientIP_part_1 = clientIP.substring(0,clientIP.lastIndexOf('.'));
   var clientIP_part_2 = clientIP.substring(clientIP.lastIndexOf('.')+1,15);
 
   
    try {
        var validIp = false;
        for (var i = 0; i < allowedIPs.length; i++) {
           var ip_part_1 = allowedIPs[i].substring(0, allowedIPs[i].indexOf('/'));
           var ip_part_start = allowedIPs[i].substring(allowedIPs[i].indexOf('/')+1, allowedIPs[i].indexOf('-'));
           var ip_part_end = allowedIPs[i].substring(allowedIPs[i].indexOf('-')+1,20);
      
           
          if (ip_part_1 == clientIP_part_1 && ip_part_start <= clientIP_part_2 && ip_part_end >= clientIP_part_2) {
                validIp = true;
            }
        }
 
        if (validIp) {} else {
</script>

%%[Redirect("https://google.com")]%%

<script runat="server">
        }
    } catch (e) {
     // You could also redirect to an error page here.
     // This catch-block catches any error within the try-block, so not only wrong IP addresses
        Write(Stringify(e));
    }
</script>

Checking the referrer URL to limit the access to specific sources

This is especially useful if your users are coming from a specific website, another Cloudpage or if the Cloudpage is integrated in Marketing Cloud as an app. I have shown how to integrate a Cloudpage as an app in an earlier post.

The principle is again quite simple. You check if the referrer URL is the one you want and only show content if it is so. With this for example you ensure that the URL is not just copied into the browser but called from the requested source. If not you can run another action. I chose to redirect the user to Google again.

If the user is coming from a Cloudpage the referrer is the domain you used for the source Cloudpage. This can be your Marketing Cloud stack domain or the domain you used in your SAP (Sender Authentication Package). In my example I want the user coming from the SFMC domain or another Cloudpage because I use it as a Marketing Cloud app. If not as I said I redirect him or her to Google (this could also be replaced by the Marketing Cloud login URL or an error message).

%%[
/*Check for referrer domain*/
 
SET @referrer = HTTPRequestHeader("Referer")
IF indexOf(@referrer, "https://mc.s10.exacttarget.com/cloud/") != "1" AND
indexOf(@referrer, "https://cloud.YOURDOMAIN.de/") != "1" AND indexOf(@referrer, "https://mc.s10.exacttarget.com/") != "1"
THEN Redirect("https://google.com") ENDIF
      
]%%

Check if the user is logged in to Marketing Cloud

I wrote about this in detail in my last article. We request the Cloudpage via the authorization server of Marketing Cloud. If we are not logged in to the Marketing Cloud, we get redirected to the login page and not to the target page. If we are logged in we get redirected to the requested page with a user specific code as an URL parameter. If we get the code, we do not redirect via the authorization endpoint again but check that the code is valid. To give you a better understanding of this process I prepared the following graphic.

And here is the associated code:

%%[

/*Validation if a user is logged in to the website
to work with attributes passed as URL parameter use 
 <input type="hidden" value="%%=v(@sessionkey)=%%" name="sessionkey">
in every form. This variable is a 1 hour token to skip the recheck.
*/

/*define security variables - change for other packages and pages*/
SET @cloudpageURL = 'https://cloud.YOURDOMAIN.COM/CLOUDPAGE'
SET @baseURI = 'YOUR_BASE_URI'
SET @clientid = 'XXXXXXXXXXXXXXXXX'
SET @clientsecret = 'XXXXXXXXXXXXXXXXX'

/*define password (64), Initialization vector (32) and salt (16) for encryption
- change for other pages*/
set @ivector = 'XXXXXXXXXXXXXXXXXXXXXXXXX'
set @salt = 'XXXXXXXXXXXXXXXX'
set @encrpswd = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

/*get code if passed with the URL*/
SET @code = RequestParameter('code')

/*check if session key is passed with the URL and decrypt it*/
Set @passedencrsessionkey = RequestParameter('sessionkey')

IF IsNullDefault(@passedencrsessionkey, 'null') != 'null' AND @passedencrsessionkey != '' THEN
SET @passedsessionkey=DecryptSymmetric(@passedencrsessionkey, 'AES', @null, @encrpswd, @null, @salt, @null, @ivector)   
ENDIF

/*create encrypted session key with 1 hour lifetime for usage in forms and redirects*/
set @newsessionkey = DateAdd(Now(),1,"H")
SET @sessionkey=EncryptSymmetric(@newsessionkey, 'AES', @null, @encrpswd, @null, @salt, @null, @ivector)

/*check if the passed session key is valid if available*/
IF IsNullDefault(@passedencrsessionkey, 'null') != 'null' AND @passedencrsessionkey != '' AND @passedsessionkey >= Now()
AND @passedsessionkey <= @newsessionkey
THEN 
/*load Cloudpage*/

/*if passed session key is not valid check if a code is available and check it via API request, redirect through auth-page if the code is not valid*/
ELSEIF IsNullDefault(@code, 'null') != 'null' AND @code != '' 
THEN 

]%%

<script runat="server">
Platform.Load("Core","1.1.1");
   
try { 
    
/* get OAuth 2.0 access token */
var code = Platform.Variable.GetValue("@code");
var payload = '{"grant_type": "authorization_code",';
payload += '"code": "' + code + '",';
payload += '"client_id": "' + Variable.GetValue("@clientid") + '",';
payload += '"client_secret": "' + Variable.GetValue("@clientsecret") + '",';
payload += '"scope": null,';
payload += '"redirect_uri": "' + Variable.GetValue("@cloudpageURL") + '"}';
var OAuth2URL = 'https://' + Variable.GetValue("@baseURI") + '.auth.marketingcloudapis.com/v2/token';
var contentType = 'application/json';
var checkCode  = HTTP.Post(OAuth2URL, contentType, payload);

}
  
catch(e) {
</script>

%%[Redirect(CONCAT('https://', @baseURI, '.auth.marketingcloudapis.com/v2/authorize?response_type=code&client_id=', @clientid, '&redirect_uri=',@cloudpageURL))]%%

<script runat="server">}</script>

%%[
/*If there is no code and no valid session key redirect through auth-page*/
ELSE
Redirect(CONCAT('https://', @baseURI, '.auth.marketingcloudapis.com/v2/authorize?response_type=code&client_id=', @clientid, '&redirect_uri=',@cloudpageURL))

ENDIF
]%%

Check out my original post about this topic to get more variations and an understanding of the detailed composition of this process.

Let’s summarize

I used all three methods to restrict Cloudpage access to our organization’s users. With the combination of them I made sure that the user accessing the Cloudpage is

  • in our network (at work or connected via VPN)
  • opening the cloudpage as an Marketing Cloud app or was forwarded from one of my cloudpages
  • logged in to his or her Marketing Cloud account

Because we show sensitive data and functions on Cloudpages it is required to have such security mechanisms. Colleagues of mine also built their own login process for Cloudpages where they save usernames and passwords encrypted in a Data Extension. Since I did not build that one on my own I can refer to this great post by Ivan about password protected Cloudpages.

How do you protect your Cloudpages? Do you have special use cases or different security scripts? Let me know in the comments!

Leave a Reply

Your email address will not be published. Required fields are marked *