Magic links with ForgeRock Access Management

When you login to Medium, Github and many other website you just have to enter your login (or email) and then you’ll receive an email with a link to log you in the website. It’s a really common thing to have to validate your email when you register to a website but now it’s becoming more and more frequent to use this mechanism as password less solution; it’s called Magic Link. Let’s see how to do it with ForgeRock Access Management.

What’s Magic Link?

When a user wants to login to the Web App, he enters his email in the Web interface, ForgeRock Access Management (AM) generates a temporary token, stores it in the user profile for a predefined period of time (15 minutes for instance), and sends a link to the user containing the login of the user and the temporary token. Once the user clicks on the link, AM checks the login and token, if both are valid then the user access is granted and the temporary token is removed from the user profile (and not valid anymore). The following diagram shows how it works.

Note: This implementation may be enhanced with additional control when the user clicks on the link (permanent cookie setting and checking, device fingerprint check, etc…).

Create it in Access Management

To implement this in AM we will use the powerful Authentication Tree technology. We will implement 2 trees.

  1. Check Tree : Collect the user login and token, checks the validity, grant access and remove the token from the user profile.

Init Tree

To implement the Init Tree, connects to AM web console and follow theses steps.

Create a script to generate the temporary token.

  1. Select your realm, browse to Scripts and click on New Script to created a new one
  2. On the script creation page, name your script GenerateMagicToken and select Decision node script for authentication trees Script Type.
  3. In the script field enters the following code and click on Save Changes.
function magicToken() {
return 'xxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) {
var r = Math.random()*16|0;
var v = r;
return v.toString(16);
});
}
var magicLink = {};
magicLink.magicToken = magicToken();
magicLink.date = new Date().toJSON();
var magicLinkString = JSON.stringify(magicLink);
sharedState.put("magicToken",magicLink.magicToken);
sharedState.put("magicLink",encodeURI(magicLinkString));
outcome = "true";

Create a script to get the username based on the email entered by the user.

  1. Select your realm, browse to Scripts and click on New Script to created a new one
  2. On the script creation page, name your script emailToUsername and select Decision node script for authentication trees Script Type.
  3. In the script field enters the following code and click on Save Changes.
var restBody = "{}";// Get a token for amadmin to search for the end-user
var uriAM = String('https://yourserver/openam/json/realms/root/authenticate');
var request = new org.forgerock.http.protocol.Request();
request.setMethod('POST');
request.setUri(encodeURI(uriAM));
request.getHeaders().add("X-OpenAM-Username","amadmin");
request.getHeaders().add("X-OpenAM-Password", "YourPass");
request.getHeaders().add("content-type","application/json");
request.getHeaders().add("Accept-API-Version","resource=2.0, protocol=1.0");
request.getEntity().setString(restBody);
var response = httpClient.send(request).get();
var jsonResult = JSON.parse(response.getEntity().getString());
// Checks if user exists in Data Store and retrieve username
var uriAMInfo = String('https://yourserver/openam/json/realms/root/users?_queryFilter=mail+eq+\"' + sharedState.get("mail") + '\"&_fields=username');
var requestInfo = new org.forgerock.http.protocol.Request();
requestInfo.setMethod('GET');
requestInfo.setUri(encodeURI(uriAMInfo));
requestInfo.getHeaders().add("iPlanetDirectoryPro",jsonResult["tokenId"]);
requestInfo.getEntity().setString(restBody);
var responseInfo = httpClient.send(requestInfo).get();
var jsonResultInfo = JSON.parse(responseInfo.getEntity().getString());
if (jsonResultInfo["result"][0]["username"] != null){
sharedState.put("username",jsonResultInfo["result"][0]["username"]);
outcome = "true";
}else {
outcome = "false";
}

Note: I don’t explain scripts in this note (maybe in another one), If you need more information, I recommend you to read ForgeRock documentation.

Create the Init Tree to initiate the magic link.

  1. Select your realm, browse to Authentication>Trees and click on Create Tree to created a new one called InitMagicLink.
  2. Add a Failure URL node in your tree and link the output to the Failure node. In the Failure URL parameter, enter /openam.
    Note: this node is used to redirect the user to a page saying that an email has been sent for him to login. In this note I just redirect the user to de default AM login page but it can be any page you want (even Google if you want).
  3. Add an Email Notify node, name it SendMagicLink, for Email Attribute parameter enter mail, for Email Subject enter Your Magic Link to login, for Message Body enter Dear {{username}}, Click on this link to authenticate: https://yourserver/openam/XUI/?service=CheckMagicLink&token={{magicToken}}&username={{username}}#login/ and then configure the other parameters to work with your SMTP server. Finally link the output to the Failure URL node.
    Note : If Email Notify node is not included OOTB of you AM you can find it in ForgeRock Marketplace.
  4. Add a Set Profil Property node, name it SaveMagicToken, configure key and value with postalAddress and magicLink. Then link the output to SendMagicLink node.
    Note 1: If Set Profile Property node is not included OOTB of you AM you can find it in ForgeRock Marketplace.
    Note 2: in this blog note I’ll use
    postalAddress attribute to store the magic token info. If you prefer, you can extend your user schema and use a dedicated attribute for this.
  5. Add a Scripted Decision node, name it GenerateMagicToken, select GenerateMagicToken script, add true as outcome and link it with the SaveMagicToken node.
  6. Add a Scripted Decision node, name it GetUserFromEmail, select emailToUsername script, add true and false as outcomes. Link the true outcome to GenerateMagicToken node and falseto Failure node.
  7. Finally, add an Input Collector node and name it EmailAddressCollector. Configure Variable Name parameter with mail, configure Prompt parameter with Please enter your email, and disable Password Input and Use Transient State parameters.Finally, link the income with the Start node and the outcome with the GetUserFromEmail node.
    Note : If Input Collector node is not included OOTB of you AM you can find it in ForgeRock Marketplace.

At the end your tree should look like the following tree.

Check Tree

To implement the CheckTree follow theses steps.

Create a script to get the username and token from the referer header.

  1. Select your realm, browse to Scripts and click on New Script to created a new one
  2. On the script creation page, name your script getUsernameAndToken and select Decision node script for authentication trees Script Type.
  3. In the script field enters the following code and click on Save Changes.
var referer = requestHeaders.get("referer").get(0);// Parse the referer to get the username and token query parameters 
var params = {};
var vars = referer.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
params[pair[0]] = decodeURIComponent(pair[1]);
}
var usernameURL = params.username;
var tokenURL = params.token;
if (usernameURL != null && tokenURL != null){
sharedState.put("username",usernameURL);
sharedState.put("token",tokenURL);
outcome = "true";
}else {
outcome = "false";
}

Create a script to get the magiclink token and date stored in the user profile, compare it with what has been provided by the user, and then remove the magiclink token and date from the user profile.

  1. Select your realm, browse to Scripts and click on New Script to created a new one
  2. On the script creation page, name your script checkMagicLink and select Decision node script for authentication trees Script Type.
  3. In the script field enters the following code and click on Save Changes.
var restBody = "{}";// Get a token for amadmin to search for the end-user
var uriAM = String('https://yourserver/openam/json/realms/root/authenticate');
var request = new org.forgerock.http.protocol.Request();
request.setMethod('POST');
request.setUri(encodeURI(uriAM));
request.getHeaders().add("X-OpenAM-Username","amadmin");
request.getHeaders().add("X-OpenAM-Password", "YourPass");
request.getHeaders().add("content-type","application/json");
request.getHeaders().add("Accept-API-Version","resource=2.0, protocol=1.0");
request.getEntity().setString(restBody);
var response = httpClient.send(request).get();
var jsonResult = JSON.parse(response.getEntity().getString());
// Checks if user exists in Data Store and retrieve the MagicToken
var uriAMInfo = String('https://yourserver/openam/json/realms/root/users?_queryFilter=uid+eq+\"' + sharedState.get("username") + '\"&_fields=postalAddress');
var requestInfo = new org.forgerock.http.protocol.Request();
requestInfo.setMethod('GET');
requestInfo.setUri(encodeURI(uriAMInfo));
requestInfo.getHeaders().add("iPlanetDirectoryPro",jsonResult["tokenId"]);
requestInfo.getEntity().setString(restBody);
var responseInfo = httpClient.send(requestInfo).get();
var jsonResultInfo = JSON.parse(responseInfo.getEntity().getString());
var jsonMagicToken = JSON.parse(decodeURI(jsonResultInfo["result"][0]["postalAddress"]));

var magicToken = jsonMagicToken.magicToken;
var tokenDate = jsonMagicToken.date;

if (magicToken==sharedState.get("token")){
var now = new Date();
var Difference_In_Time = now.getTime() - (new Date(tokenDate)).getTime();
if (Math.round(Difference_In_Time/(1000 * 60)) < 5){
//If the user uses the link before 5 minutes we remove the magic
uriAMInfo = String('https://yourserver/openam/json/realms/root/users/' + sharedState.get("username"));
requestInfo = new org.forgerock.http.protocol.Request();
requestInfo.setMethod('PUT');
requestInfo.setUri(encodeURI(uriAMInfo));
requestInfo.getHeaders()
.add("iPlanetDirectoryPro",jsonResult["tokenId"]);
requestInfo.getHeaders()
.add("Accept-API-Version","resource=2.0, protocol=1.0");
requestInfo.getHeaders()
.add("Content-Type","application/json");
requestInfo.getEntity()
.setString(JSON.stringify({"postalAddress":""}));
responseInfo = httpClient.send(requestInfo).get();

outcome = "true";
} else {
outcome = "false";
}
} else {
outcome = "false";
}

Note : the bold line from the script before is where you define the number of minute the link is valid. In this example it is defined to 5 minutes.

Create the Check Tree to authenticate the use by checking the token and username validity.

  1. Select your realm, browse to Authentication>Trees and click on Create Tree to created a new one called CheckMagicLink.
  2. Add a Success node in your tree.
  3. Add a Scripted Decision node, name it CheckMagicToken, select checkMagicLink script, add true and false as outcomes. Link the true outcome to Success node and falseto Failure node.
  4. Add a Scripted Decision node, name it GetRefererInfo, select getUsernameAndToken script, add true and false as outcomes. Link the true outcome to CheckMagicToken node and falseto Failure node.

At the end your tree should look like the following tree.

Magic Link in action from the user perspective

The next figure shows the result in action from the user perspective.

Conclusion

With this step by step implementation of Magic Links with ForgeRock Access Management we’ve seen how easy it is to implement any user journey you want with ForgeRock’s Trees.

To strengthen this implementation, I recommend enriching these two trees with the use of persistent cookies or device verification mechanism. These components may be the subject of a future article!

Sales Engineer at ForgeRock www.linkedin.com/in/sorluc