Magic links with ForgeRock Access Management — Before v7
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…).
Note: Since ForgeRock version 7 a new node has been introduced : Email Suspend Node. It makes it even more easy to implement magic links in one tree.
Documentation here: https://backstage.forgerock.com/docs/am/7/authentication-guide/authn-suspended.html#authn-suspended
Create it in Access Management
To implement this in AM we will use the powerful Authentication Tree technology. We will implement 2 trees.
- Init Tree: The first tree will generate a token, store it in the user profile and send the magic link to the user.
- 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.
- Select your realm, browse to Scripts and click on New Script to create a new one
- On the script creation page, name your script GenerateMagicToken and select Decision node script for authentication trees Script Type.
- 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.
- Select your realm, browse to Scripts and click on New Script to create a new one
- On the script creation page, name your script emailToUsername and select Decision node script for authentication trees Script Type.
- 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.
- Select your realm, browse to Authentication>Trees and click on Create Tree to create a new one called InitMagicLink.
- 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 the default AM login page but it can be any page you want (even Google if you want). - Add an Email Notify node, name it SendMagicLink, for Email Attribute parameter enter
mail
, for Email Subject enterYour Magic Link to login
, for Message Body enterDear {{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. - Add a Set Profil Property node, name it SaveMagicToken, configure key and value with
postalAddress
andmagicLink
. 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 usepostalAddress
attribute to store the magic token info. If you prefer, you can extend your user schema and use a dedicated attribute for this. - Add a Scripted Decision node, name it GenerateMagicToken, select GenerateMagicToken script, add
true
as outcome and link it with the SaveMagicToken node. - Add a Scripted Decision node, name it GetUserFromEmail, select emailToUsername script, add
true
andfalse
as outcomes. Link thetrue
outcome to GenerateMagicToken node andfalse
to Failure node. - Finally, add an Input Collector node and name it EmailAddressCollector. Configure Variable Name parameter with
mail
, configure Prompt parameter withPlease 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.
- Select your realm, browse to Scripts and click on New Script to create a new one
- On the script creation page, name your script getUsernameAndToken and select Decision node script for authentication trees Script Type.
- 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.
- Select your realm, browse to Scripts and click on New Script to create a new one
- On the script creation page, name your script checkMagicLink and select Decision node script for authentication trees Script Type.
- 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.
- Select your realm, browse to Authentication>Trees and click on Create Tree to create a new one called CheckMagicLink.
- Add a Success node in your tree.
- Add a Scripted Decision node, name it CheckMagicToken, select checkMagicLink script, add
true
andfalse
as outcomes. Link thetrue
outcome to Success node andfalse
to Failure node. - Add a Scripted Decision node, name it GetRefererInfo, select getUsernameAndToken script, add
true
andfalse
as outcomes. Link thetrue
outcome to CheckMagicToken node andfalse
to 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!