Marketing Cloud User Validation on Cloudpages

Marketing Cloud User Validation on Cloudpages

In this article I will explain how you can check if the user that is opening a Cloudpage is logged in to Marketing Cloud and restrict access if there is no active session. Therefore we will route users via the SFMC authorization server to get a user specific code. Then we will validate that code using the API to restrict Cloudpage access for users that are not logged in. In the next step we will create a session key that reduces the redirects and so ensures that our functions remain working.

I will also show how to identify the logged in user to use the information for personalization or to trigger API calls on behalf of the Marketing Cloud user. We will achieve this by the use of AMPScript and Javascript.

In the past there were different solutions to check the SFMC user who is on a cloudpage. But by now simple functions like this one do not work anymore on current accounts. After some discussions with Salesforce it also does not seem to be possible to read any existing Marketing Cloud cookie and get the information from there. So doing more investigation we came up with the following.

Step 1: Create an Installed Package for the authentication

The first thing we need to do is to get a code for the user who is logged in. Therefore we use the authorization server of Marketing Cloud. The principle is more or less documented in the Salesforce documentation. In this scenario you call the Cloudpage as a redirect via the server. If you are logged in, the server forwards you to your target page adding a code to the URL. If you are not logged in, it asks you for your credentials.

So lets start with the creation of an Installed Package via Marketing Cloud Setup. In your package you will need an API Integration component for the user check.

This component needs to be of the type “Web App”. Add the URL of the Cloudpage you want to secure as a redirect URI and save the component. As long as you are not using the package for other API requests you do not need to put any objects into scope.

Now the Installed Package should look like this:

Step 2: Get an authorization code for the user

Now we can go to our Cloudpage. I start with a code block for the AMPScript and Javascript code. 

The request for the authorization code should look like this:

https://YOUR_BASE_URI.auth.marketingcloudapis.com/v2/authorize?response_type=code&client_id=XXXXXXXXXXX&redirect_uri=https://cloud.YOURDOMAIN.com/CLOUDPAGE

Use this request as an AMPScript redirect when the Cloudpage gets opened. The Marketing Cloud authorization server will check the login and redirect you back to the Cloudpage. While doing the redirect it will add a code to your URL like:

https://cloud.YOURDOMAIN.COM/CLOUDPAGE?code=a1b2c3d4e5

With the next page load you need to check if a code is added to your URL through the authorization server. You can easily get the code from the URL via

SET @code = RequestParameter(‘code’)

Now use the code as the condition to do another redirect or not.

IF IsNullDefault(@code, ‘null’) == ‘null’ OR @code == ” THEN 
Redirect(https://YOUR_BASE_URI.auth.marketingcloudapis.com/v2/authorize?response_type=code&client_id=XXXXXXXXXXX&redirect_uri=https://cloud.YOURDOMAIN.COM/CLOUDPAGE)
ENDIF

Step 3: Check the authorization code validity

The existance of a code indicates that a Marketing Cloud user is accessing the Cloudpage. Of course this can be faked by adding just any code to the URL. So we need to check if the code is valid. We do this by doing a token request with the code. If the code is valid we get a token back. If it is not valid we get an error message. In case of an error we can trigger another action like a redirect to the login page. Therefore I use the try-catch statement. All together my code looks like this:

%%[ 
/*fill the following variables with your data*/
SET @cloudpageURL = 'https://cloud.YOURDOMAIN.COM/CLOUDPAGE'
SET @baseURI = 'YOUR_BASE_URI'
SET @clientid = 'XXXXXXXXXXXXXXXXX'
SET @clientsecret = 'XXXXXXXXXXXXXXXXX'

SET @code = RequestParameter('code')
IF IsNullDefault(@code, 'null') == 'null' or @code == '' THEN
Redirect(CONCAT('https://', @baseURI, '.auth.marketingcloudapis.com/v2/authorize?response_type=code&client_id=', @clientid, '&redirect_uri=',@cloudpageURL))
ENDIF
]%%

<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>

The whole process of a Cloudpage opened using this authentication would look like this:

Step 4: Create and pass a session key

For many cases the code above is finished and can be used like it is. In my case I have a form on the Cloudpage that reloads the page while passing some more URL parameters. The problem is in this case that on the one hand you do not want to check the user with every page load and on the other hand you lose other URL parameters through the redirect. So I needed a key that tells my page that I am an authorized Marketing Cloud user to prevent the redirect.

My first idea was to set a cookie using AMPScript to solve the problem. Because I used the Cloudpage as an app my browser classified the cookie as a 3rd party cookie and blocked it by default. So I quickly discarded this idea again. It was also not possible to save the key in a browser session using the sessionStorage property of the window object. In this case the problem is to exchange necessary data between the server side and the client side because server side scripts are running first and so can not simply use the client side data.

So the new plan was to pass an URL parameter with the reload that says “Marketing Cloud user – no code validation needed”. This could not be just a key or parameter you can see in the browser console that any user could copy and adapt anytime. It needed to be safe and finite. I decided to go with a timeframe or more a specific time where the session key loses its validity. In my case it will be one hour after this key was created. To make it more secure I decided to use an AES encryption.

Let me show you the steps

You start with the creation of the session key:

SET @newsessionkey = DateAdd(Now(),1,”H”)

Sure a combination of the time and another attribute could be even safer. For me this is enough for now.

Then you encrypt it using the EncryptSymetric AMPScript function:

SET @sessionkey=EncryptSymmetric(@newsessionkey, ‘AES’, @null, @encrpswd, @null, @salt, @null, @ivector)

Now you can add it to any page request or inject it in a form submit as a hidden field like this:

<input type=”hidden” value=”%%=v(@sessionkey)=%%” name=”sessionkey”>

Step 5: Receive and check a passed session key

Now that we pass the session key we need to extend the existing code to verify it. After a pageload you need to request the session key, decrypt it and use it in a conditional statement. Always make sure that the parameter is filled to avoid server errors.

SET @passedencrsessionkey = RequestParameter(‘sessionkey’)

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

IF IsNullDefault(@passedencrsessionkey, ‘null’) != ‘null’ AND @passedencrsessionkey != ” AND @passedsessionkey >= Now()
AND @passedsessionkey <= @newsessionkey
THEN

/*load Cloudpage*/

Now I bring it all together with the existing process to check the session key prior to the redirect that gives us the authorization code:

Adding the session key script to the existing code it will look like this:

%%[

/*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

Only use hexadecimal characters (0-9, A-F) for salt and Initialization vector*/
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
]%%

Step 6: Retrieve the username to do a personalization (optional)

When you do not skip the redirect via the Marketing Cloud authorization server, you can use the token you received in step 3 again to do the Userinfo API call.

Normally this request would return the details of the API user. However, since we requested the token with the code, you get the Marketing Cloud user’s information instead. You just need to do a GET request for https://YOUR_BASE_URI.auth.marketingcloudapis.com/v2/userinfo

%%[ 
SET @cloudpageURL = 'https://cloud.YOURDOMAIN.COM/CLOUDPAGE'
SET @baseURI = 'YOUR_BASE_URI'
SET @clientid = 'XXXXXXXXXXXXXXXXX'
SET @clientsecret = 'XXXXXXXXXXXXXXXXX'

SET @code = RequestParameter('code')
IF IsNullDefault(@code, 'null') == 'null' or @code == '' THEN
Redirect(CONCAT('https://', @baseURI, '.auth.marketingcloudapis.com/v2/authorize?response_type=code&client_id=', @clientid, '&redirect_uri=',@cloudpageURL))
ENDIF
]%%

<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);

/* make the API call and get the response */
var tokenObj = Platform.Function.ParseJSON(checkCode["Response"][0]);
var accessToken = tokenObj.access_token;
var headerNames = ["Authorization"];
var headerValues = ["Bearer " + accessToken];
var urlCreate = 'https://' + Variable.GetValue("@baseURI") + '.auth.marketingcloudapis.com/v2/userinfo';
var userinfo = HTTP.Get(urlCreate, headerNames, headerValues);
var userinfojson = Platform.Function.ParseJSON(userinfo["Content"]);
Variable.SetValue("@preferred_username",userinfojson.user.preferred_username);
}
  
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>

%%=v(@preferred_username)=%%

Then you extract the username (or any other information you like) from the response. Use it to do your personalization, provide user specific content or whatever else you want to do with the knowledge who he or she is. You find the attributes containded in the response in the documentation.


I used the shown user validation to secure Cloudpages that show reports and logs for Marketing Cloud users. Since I am not a professional developer this seemed to be the best solution for me to do an user check. Do you use Cloudpages for internal users and have you found other ways to log a browser session? Let me know in the comments!

11 thoughts on “Marketing Cloud User Validation on Cloudpages

  1. Great example. If I understand correctly we need to create a web-app for each cloud page as the redirect_uri needs to be the same as the cloud page url (or the cloud page we are trying to access).

    I am working on a similar use case that the cloud page be visible only to authenticated users. I want to create a code snippet that I can embed in all cloud pages but to access them, the user needs to authenticate.

    My use case:
    Lets say my cloud page is : pub.s5.et.com/welcome

    In this page, I want to include a generic SSJS script to check for authenticated user and if authenticated show the cloud content, if not show an unauthorized page.

    Greatly appreciate any thoughts on this. Thanks

    1. Hi Raj,
      it should also work with one Web App for all the pages. You need to add multiple URIs to that. Mind that SFMC will need a few minutes to adapt the changes in its backend. For the URL you probably could use RequestParameter(‘PageURL’)

  2. Hey Tony,

    Thanks a ton for this awesome info.

    Since we are already using installed package. Can we set permissions to specific users based on licensing on the installed package.

    Just trying to understand how to leverage that.

    1. Hi Ibrahim,
      if you use the Cloudpage as an app and check the referrer so that the Cloudpage can’t be opened outside of Marketing Cloud you could limit the access on the Cloudpage itself. If the page is accessible the licensing of the package did not change if the user could open the page using the url or not.

  3. Would it be possible to use this same workflow, but instead have a page or pop up asking for PII that then validates that data in Marketing Cloud to then redirect them to the desired cloud page?

    1. Hi Rachel,
      Sure that would be possible. You could compare values you may have in a Data Extension with some user input. Therefore I would recommend to use encryption functions as well.

  4. Hi Tony,

    great content, thanks a lot!
    I succesfully applied this to a cloudpage of mine as well.
    When opening the cloudpage, everything looks as it should – but unfortunately the script of my original application on that cloudpage (embedded via a content block) does not return any results anymore.
    Once I remove the ‘protection’-script from the beginning of the cloudpage, the cloudpage and its script are working again as intended.
    I know it is a long shot, but do you have any idea what might cause that issue?

    Thanks a lot.

    1. Hi Lukas,
      without knowing what your script is doing, I could imagine that your script is passing some parameters that get lost when the new script is doing a redirect. If so I would recommend you to add the verification code that my script is creating to any redirects your content block is doing. Maybe you want to adapt my script for testing so that you avoid the redirection. If there are not any typos, this is the main thing I would check.

      1. Thanks, Tony!
        The content block with the script basically creates a form, where one can insert eg. a Data extension name and then it searches (using WSproxy) in objectType = “QueryDefinition”.
        The value from the form is added as value to the filter.
        I figured out that it, when I replace the search string from the form in the with a static value in the filter code, everything works as intended.

        Example:
        part of the HTML for the form:

        And then in the WSproxy, I do the following:
        var DEval = Request.GetQueryStringParameter(“searchstringval”);
        objectType = “QueryDefinition”,
        cols = [“Name”,”CustomerKey”],
        filter = { Property: “QueryText”, SimpleOperator: “like”, Value: searchstringval};

Comments are closed.

Comments are closed.