This article explains how to protect a Salesforce Marketing Cloud page with a login and password with the 2 factor authentication technique using AMPscript and server-side Javascript.
How does it work ?
For starters, let’s discuss how this flow actually works.
A user arrives on the password protected page. The script on this page will look for a specific cookie in the browser and cross-check the token that this cookie contains against a record in a Token Data Extension, to determine if this token exists and was created less than 24 hours ago.
If the cookie is valid, the script displays the content of the page, otherwise the user is redirected to the login page.
The login page asks the user for a login and password, which are crossed-checked against the records in the User Data Extension to determine if the user exists.
If so, a unique token is generated for that user and is sent by email.
The user is then redirected to a form that asks for a token. There is a choice to make: copy/paste the token from the email or click on the link in the email.
Either way, the token is then cross-checked with a record in the Token Data Extension to determine if such record exists and has been created less than 24 hours ago.
If the record checks out, the cookie with the token is created and the user is redirected back to the protected page and is considered authenticated.
Data Extensions
We need 3 Data Extensions in this case.
Users
This Data Extension contains the credentials of the users that will be able to authenticate. Note that they are created/imported manually.
Id | Text | 50 | Primary Key |
EmailAddress | 254 | Not nullable | |
Username | Text | 50 | Not nullable |
Password | Text | 50 | Not nullable |
Tokens
This Data Extension contains the tokens that are generated after the first login.
Id | Text | 50 | Primary Key |
Token | Text | 50 | Not nullable |
Referer | Text | 4000 | Not nullable |
CreatedDate | Date | Not nullable |
TSD_Tokens
This Data Extension is a TriggeredSend Data Extension that stores the data to be sent in the email.
SubscriberKey | Text | 50 | Primary Key |
EmailAddress | EmailAddress | 254 | Not nullable |
Token | Text | 50 | Not nullable |
Referer | Text | 4000 | Not nullable |
Cloud pages
There are 3 cloud pages to be created.
Protected page
The SSJS and AMPscript codes before the DOCTYPE tag are responsable for starting the login flow. This section of code is reusable, which means you can import it as an HTML block in the Content Builder and use it on any page by calling the ContentBlockbyId function.
The code basically checks the presence of a cookie, looks in a Data Extension if the token inside that cookie exist in a record and verifies that the created date of the record is less than 24 hours. If the cookie doesn’t comply with the conditions, the user is redirected to the login page with the URL of the page as a parameter (referer).
<script runat="server">
Platform.Load("core", "1.1");
var token = Platform.Request.GetCookieValue('userlg')
Platform.Variable.SetValue("@token", token);
</script>
%%[
SET @loginPageURL = "https://my-mc.com/login-page"
SET @currentPageURL = RequestParameter('PAGEURL')
SET @token = @token
SET @today = SystemDateToLocalDate(NOW())
SET @createdDate = Lookup("Tokens","CreatedDate","Token",@token)
IF EMPTY(@createdDate) OR @today > DateAdd(@createdDate, 1, "D" ) THEN
Redirect(concat(@loginPageURL,"?referer=",@currentPageURL))
ENDIF
]%%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Secured page</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
Login page
The login page is a bit more complex. It manages 4 different situations in which the user is redirected to the login page.
- The user wants to access a password protected page.
- The user entered wrong credentials or the token is incorrect.
- The user has just received a token that needs verification.
- The user clicked on an email generated link with the token passed as a parameter.
Whatever the situation, the outcome remains the same: the user is redirected to the dispatch page that will manage the end result.
<p>
%%[
SET @dispatchPageURL = "https://my-mc.com/dispatch-page"
SET @token = QueryParameter("token")
SET @istokensent = QueryParameter("istokensent")
SET @invalidpass = QueryParameter("invalidpass")
SET @invalidtoken = QueryParameter("invalidtoken")
SET @referer = QueryParameter("referer")
IF EMPTY(@istokensent) AND EMPTY(@token) THEN
IF @invalidpass == 1 THEN
OUTPUT(CONCAT("Invalid username or password"))
ENDIF
IF @invalidtoken == 1 THEN
OUTPUT(CONCAT("Token expired or missing"))
ENDIF
]%%
</p>
<form method="post" action="%%=v(@dispatchPageURL)=%%">
<label for="username">User name *</label>
<input type="text" name="username" minlength="1">
<br>
<label for="pass">Password *</label>
<input type="password" name="pass" minlength="1">
<br>
<input type="hidden" name="referer" value="%%=v(@referer)=%%">
<br>
<button>Send</button>
</form>
%%[ ELSEIF EMPTY(@istokensent) AND NOT EMPTY(@token) THEN
Redirect(concat(@dispatchPageURL,"?token=",@token,"&referer=",@referer))
ELSE ]%%
<p>Please check your email and copy-paste your token here</p>
<form method="post" action="%%=v(@dispatchPageURL)=%%">
<label for="token">Secret token *</label>
<input type="text" name="token" minlength="1">
<br>
<input type="hidden" name="referer" value="%%=v(@referer)=%%">
<br>
<button>Log in</button>
</form>
%%[ ENDIF ]%%
Dispatch page
The dispatch page is the heart of our script. It manages all the possible situations and decides whether we redirect the user back to the login page or the protected page.
First, the script verifies if the login, password and referer were submitted or sent as parameters. If this is the case, it means we are in the situation #1 and script verifies the user by cross-checking with a record in the Users Data Extension. If a record exists, an email is sent to the user with a randomly generated token and the user is redirected to the login page, as per login situation #3.
When the token was submitted or sent as a parameter from the login page to the dispatch page, the script verifies that a record with that token exists in the Tokens Data Extension and if the token was generated less than 24 hours ago. If both conditions are satisfied, the script considers that the user can be logged in and creates the cookie with the token in its value. After that, the user is redirected to the protected page and the flow ends.
In any other case, the user is redirected to the login page with an error type as URL parameter (login situation #2).
%%[
SET @loginPageURL = "https://my-mc.com/login-page"
SET @username = RequestParameter("username")
SET @pass = RequestParameter("pass")
SET @token = RequestParameter("token")
SET @referer = RequestParameter("referer")
SET @today = SystemDateToLocalDate(NOW())
SET @TriggeredSendExternalKey = "112233"
IF NOT EMPTY(@username) AND NOT EMPTY(@pass) AND NOT EMPTY(@referer) THEN
SET @rows = LookupRows("Users","Username",@username,"Password",@pass)
IF RowCount(@rows) > 0 THEN
SET @Id = Field(Row(@rows, 1), 'Id')
SET @Email = Field(Row(@rows, 1), 'Email')
ENDIF
IF NOT EMPTY(@Id) AND NOT EMPTY(@Email) THEN
SET @token = GUID()
UpsertDE("Tokens",1,"Id",@Id,"Token",@token,"Referer",@referer,"CreatedDate",@today)
SET @TriggeredSend = CreateObject("TriggeredSend")
SET @TriggeredSendDefinition = CreateObject("TriggeredSendDefinition")
SetObjectProperty(@TriggeredSendDefinition, "CustomerKey", @TriggeredSendExternalKey)
SetObjectProperty(@TriggeredSend, "TriggeredSendDefinition", @TriggeredSendDefinition)
SET @TriggeredSendSubscriber = CreateObject("Subscriber")
SetObjectProperty(@TriggeredSendSubscriber, "EmailAddress", @Email)
SetObjectProperty(@TriggeredSendSubscriber, "SubscriberKey", @Email)
SET @tkn = CreateObject("Attribute")
SetObjectProperty(@tkn, "Name", "Token")
SetObjectProperty(@tkn,"Value", @token)
AddObjectArrayItem(@TriggeredSend, "Attributes", @tkn)
SET @ref = CreateObject("Attribute")
SetObjectProperty(@ref, "Name", "Referer")
SetObjectProperty(@ref,"Value", @referer)
AddObjectArrayItem(@TriggeredSend, "Attributes", @ref)
AddObjectArrayItem(@TriggeredSend, "Subscribers", @TriggeredSendSubscriber)
SET @TriggeredSend_statusCode = InvokeCreate(@TriggeredSend, @TriggeredSend_statusMsg, @errorCode)
IF @TriggeredSend_statusCode != "OK" THEN
RaiseError(@TriggeredSend_statusMsg, 0, @TriggeredSend_statusCode, @errorCode)
ENDIF
Redirect(concat(@loginPageURL,"?istokensent=",1,"&referer=",@referer))
ELSE
Redirect(concat(@loginPageURL,"?invalidpass=",1,"&referer=",@referer))
ENDIF
ELSEIF NOT EMPTY(@token) THEN
SET @createdDate = Lookup("Tokens","CreatedDate","Token",@token)
IF NOT EMPTY(@createdDate) AND @today < DateAdd(@createdDate, 1, "D") THEN
SET @endDate = DateAdd(@today, 1, "D")
]%%
<script runat="server">
Platform.Load("core", "1.1");
var endDate = Platform.Variable.GetValue("@endDate");
var token = Platform.Variable.GetValue("@token");
Platform.Response.SetCookie("userlg", token, endDate, true);
</script>
%%[
Redirect(CONCAT(@referer))
ELSE
Redirect(concat(@loginPageURL,"?invalidtoken=",1,"&referer=",@referer))
ENDIF
ELSE
Redirect(concat(@loginPageURL,"?invalidpass=",1,"&referer=",@referer))
ENDIF
]%%
Triggered send
The TriggeredSend interaction is triggered by the dispatch page when a token is generated and needs to be communicated to the user. The email message contains the token as well as a link that allows to pass it to the login or the dispatch page directly, thus completing the 2 factor authentication.
%%[
SET @token = AttributeValue("Token")
SET @referer = AttributeValue("Referer")
]%%
<p>
Please copy/paste this token: <mark>%%=v(@token)=%%</mark>
</p>
<p>
Or <a href="%%=RedirectTo(concat('https://my-mc.com/dispatch-page?token=',@token,'&referer=',@referer))=%%">click here to use the token</a>
</p>
Considerations
Please note that I don’t encrypt the data when I’m passing or sending it from one page to another or from the email to a page. It’s up to you to add the encryption of your choice for extra security.
Feel free to decrease the token validity time as well, 24 hours is a bit much.
Keep in mind that Marketing Cloud doesn’t provide us with a guaranteed solution to secure the Cloud pages out-of-the-box and therefore the method I’m describing in this article should be considered more as a proof of concept than a verified and 100% secure solution.
Have I missed anything?
Please poke me with a sharp comment below or use the contact form.