Hi Eduardo,
Here is an example in creating the JWT - note this example is achieving a forgotten password journey using the IDM mail REST API, in Identity Cloud - but this will give you an idea; note that this refers to two config placeholders (ESV in identity cloud), one for the HMAC key, and the other for the magic link URL (which invokes the second journey with the service parameter).
/*
- Data made available by nodes that have already executed are available in the sharedState variable.
- The script should set outcome to either "true" or "false".
*/
var config = {
issuer: "myissuer",
audience: "myaudience",
validityMinutes: 1,
nodeName: "***createJwt-"
};
var claims = {
uid: sharedState.get("_id")
};
var fr = JavaImporter(
org.forgerock.openam.auth.node.api.Action,
java.util.UUID,
java.time.Clock,
java.time.temporal.ChronoUnit,
javax.crypto.spec.SecretKeySpec,
org.forgerock.json.jose.builders.JwtBuilderFactory,
org.forgerock.json.jose.jws.JwsAlgorithm,
org.forgerock.json.jose.jws.handlers.SecretHmacSigningHandler,
org.forgerock.json.jose.jwt.JwtClaimsSet,
org.forgerock.secrets.SecretBuilder,
org.forgerock.secrets.keys.SigningKey,
org.forgerock.util.encode.Base64,
org.forgerock.json.jose.jws.SignedJwt,
org.forgerock.json.jose.jws.EncryptedThenSignedJwt,
org.forgerock.json.jose.jwe.SignedThenEncryptedJwt,
org.forgerock.secrets.keys.VerificationKey,
javax.crypto.spec.SecretKeySpec,
org.forgerock.json.jose.jwe.JweAlgorithm,
org.forgerock.json.jose.jwe.EncryptionMethod
);
logger.message(config.error + ": node executing");
outcome = "true";
var secret = systemEnv.getProperty("esv.hmac.jwt.signing");
var secretBytes = fr.Base64.decode(secret);
var secretBuilder = new fr.SecretBuilder();
secretBuilder.secretKey(new javax.crypto.spec.SecretKeySpec(secretBytes, "Hmac"));
secretBuilder.stableId(config.issuer).expiresIn(1, fr.ChronoUnit.MINUTES, fr.Clock.systemUTC());
var signingKey = fr.SigningKey(secretBuilder);
var signingHandler = new fr.SecretHmacSigningHandler(signingKey);
var iat = new Date();
var iatTime = iat.getTime();
var jwtClaims = new fr.JwtClaimsSet();
jwtClaims.setIssuer(config.issuer);
jwtClaims.addAudience(config.audience);
jwtClaims.setIssuedAtTime(new Date());
jwtClaims.setExpirationTime(new Date(iatTime + (config.validityMinutes * 60 * 1000)));
jwtClaims.setClaims(claims);
var jwt = null;
jwt = new fr.JwtBuilderFactory()
.jws(signingHandler)
.headers()
.alg(fr.JwsAlgorithm.HS512)
.done()
.claims(jwtClaims)
.build();
sharedState.put("jwt", jwt);
logger.message(config.nodeName + ": node exit");
Then creating the actual magic link and sending it via mail:
/*
- Data made available by nodes that have already executed are available in the sharedState variable.
- The script should set outcome to either "true" or "false".
*/
/**
* @file This script sends a HOTP to a user via the IDM SendGrid Email Templating Service
* NOTE - The use of SendGrid is not supported in Production and furthermore this service will be removed in due course.
* @version 0.1.0
* @keywords email mail hotp sharedState transientState templateService
*/
/**
* Environment specific config
*/
var idmEndpoint = "https://<idm-url>/openidm/external/email?_action=sendTemplate";
/**
* Full Configuration
*/
var config = {
ACCESS_TOKEN_STATE_FIELD: "idmAccessToken",
templateID: "magicLink",
nodeName: "***magicLinkEmailTemplateService"
};
/**
* Node outcomes
*/
var NodeOutcome = {
PASS: "sent",
FAIL: "noMail",
ERROR: "error"
};
var fr = JavaImporter(
org.forgerock.util.encode.Base64,
java.lang.String
);
/**
* Log an HTTP response
*
* @param {Response} HTTP response object
*/
function logResponse(response) {
logger.message(config.nodeName + ": Scripted Node HTTP Response: " + response.getStatus() + ", Body: " + response.getEntity().getString());
}
/**
* Send email via the IDM Email Service
*
* @param {string} username - username of the user retrieved from sharedState
* @param {string} accessToken - Access Token retrieved from transientState
* @param {string} hotp - HOTP retrieved from sharedState
* @param {string} mail - mail attribute retrieved from the idRepository
*/
function sendMail(username, accessToken, link, mail) {
var response;
logger.message(config.nodeName + ": Sending email via the IDM email templating service for user: " + username + " with mail address: " + mail + " and JWT: " + jwt);
logger.message(config.nodeName + ": Using this IDM email template: " + config.templateID);
try {
var request = new org.forgerock.http.protocol.Request();
var requestBodyJson = {
"templateName": config.templateID,
"to": mail,
"object": {
// "givenName": givenName,
"link": link
}
};
request.setMethod('POST');
request.setUri(idmEndpoint);
request.getHeaders().add("Authorization", "Bearer " + accessToken);
request.getHeaders().add("Content-Type", "application/json");
request.setEntity(requestBodyJson);
response = httpClient.send(request).get();
}
catch (e) {
logger.error(config.nodeName + ": Unable to call IDM Email endpoint. Exception: " + e);
return NodeOutcome.ERROR;
}
logResponse(response);
if (response.getStatus().getCode() === 200) {
logger.message(config.nodeName + ": Email sent for user: " + username + " with email: " + mail);
return NodeOutcome.PASS;
}
else if (response.getStatus().getCode() === 401) {
logger.error(config.nodeName + ": Access token is invalid: " + response.getStatus() + " for user: " + username);
return NodeOutcome.ERROR;
}
else if (response.getStatus().getCode() === 400) {
logger.error(config.nodeName + ": Unable to retrieve template. HTTP Result: " + response.getStatus() + " for template: " + config.templateID);
return NodeOutcome.ERROR;
}
else if (response.getStatus().getCode() === 404) {
logger.error(config.nodeName + " IDM Email endpoint not found. HTTP Result: " + response.getStatus() + " for idmEndpoint: " + idmEndpoint);
return NodeOutcome.ERROR;
}
//Catch all error
logger.error(config.nodeName + ": HTTP 5xx or Unknown error occurred. HTTP Result: " + response.getStatus());
return NodeOutcome.ERROR;
}
/**
* Node entry point
*/
logger.message(config.nodeName + ": node executing");
var username;
var accessToken;
var jwt;
var mail;
//var givenName;
//if (!(username = sharedState.get("userName"))) {
// logger.error(config.nodeName + ": Unable to retrieve username from sharedState :" + sharedState);
// outcome = NodeOutcome.ERROR;
//}
if (!(username = sharedState.get("_id"))) {
logger.error(config.nodeName + ": Unable to retrieve username from sharedState :" + sharedState);
outcome = NodeOutcome.ERROR;
}
else if (!(accessToken = transientState.get(config.ACCESS_TOKEN_STATE_FIELD))) {
logger.error(config.nodeName + ": Unable to retrieve Access Token from transientState");
outcome = NodeOutcome.ERROR;
}
else if (!(jwt = sharedState.get("jwt"))) {
logger.error(config.nodeName + ": Unable to retrieve the JWT from sharedState");
outcome = NodeOutcome.ERROR;
}
//else if (!(mail = sharedState.get("mail"))) {
// logger.error(config.nodeName + ": Unable to retrieve mail attribute from the sharedState");
// outcome = NodeOutcome.FAIL;
//}
else if (!(mail = String(idRepository.getAttribute(username, "mail").toArray()[0].toString()))) {
logger.error(config.nodeName + ": Unable to retrieve mail attribute from the idRepository");
outcome = NodeOutcome.FAIL;
}
//else if (!(givenName = sharedState.get("givenName"))) {
// logger.error(config.nodeName + ": Unable to retrieve givenName attribute from the idRepository");
// outcome = NodeOutcome.FAIL;
//}
else {
var link = systemEnv.getProperty("esv.magic.link.url") + "&magiclink=" + fr.Base64.encode(fr.String(jwt).getBytes());
outcome = sendMail(username, accessToken, link, mail);
}
Then in the resuming journey, first collect the jwt from the parameters from a scripted decision node:
/*
- Data made available by nodes that have already executed are available in the sharedState variable.
- The script should set outcome to either "true" or "false".
*/
outcome = "true";
sharedState.put("jwt", requestParameters.get("magiclink").get(0));
Then next scripted decision node, validate it:
/*
- Data made available by nodes that have already executed are available in the sharedState variable.
- The script should set outcome to either "true" or "false".
*/
var config = {
issuer: "myissuer",
audience: "myaudience",
validityMinutes: 1,
nodeName: "validateJwt-*** "
};
var nodeOutcome = {
ERROR: "error",
INVALID: "invalid",
EXPIRED: "expired",
TRUE: "true"
};
var fr = JavaImporter(
org.forgerock.openam.auth.node.api.Action,
java.util.UUID,
java.time.Clock,
java.time.temporal.ChronoUnit,
javax.crypto.spec.SecretKeySpec,
org.forgerock.json.jose.builders.JwtBuilderFactory,
org.forgerock.json.jose.jws.JwsAlgorithm,
org.forgerock.json.jose.jws.handlers.SecretHmacSigningHandler,
org.forgerock.json.jose.jwt.JwtClaimsSet,
org.forgerock.secrets.SecretBuilder,
org.forgerock.secrets.keys.SigningKey,
org.forgerock.util.encode.Base64,
org.forgerock.json.jose.jws.SignedJwt,
org.forgerock.json.jose.jws.EncryptedThenSignedJwt,
org.forgerock.json.jose.jwe.SignedThenEncryptedJwt,
org.forgerock.secrets.keys.VerificationKey,
javax.crypto.spec.SecretKeySpec,
org.forgerock.json.jose.jwe.JweAlgorithm,
org.forgerock.json.jose.jwe.EncryptionMethod,
java.lang.String
);
outcome ="true";
var jwt64String = sharedState.get("jwt");
var secret = systemEnv.getProperty("esv.hmac.jwt.signing");
if (jwt64String && secret) {
var secretBytes = fr.Base64.decode(secret);
var secretBuilder = new fr.SecretBuilder();
secretBuilder.secretKey(new javax.crypto.spec.SecretKeySpec(secretBytes, "Hmac"));
secretBuilder.stableId(config.issuer).expiresIn(1, fr.ChronoUnit.MINUTES, fr.Clock.systemUTC());
var verificationKey = new fr.VerificationKey(secretBuilder);
var jwtString = new fr.String(fr.Base64.decode(jwt64String));
logger.message(config.nodeName + " JWT string is " + jwtString);
var jwt = new fr.JwtBuilderFactory().reconstruct(jwtString, fr.SignedJwt);
var verificationHandler = new fr.SecretHmacSigningHandler(verificationKey);
if (!jwt.verify(verificationHandler)) {
logger.message(config.nodeName + "JWT signature did not verify");
outcome = nodeOutcome.INVALID;
} else {
var jwtClaims = jwt.getClaimsSet();
var jwtIssuer = jwtClaims.getIssuer();
var jwtIssuedAt = jwtClaims.getIssuedAtTime();
var jwtExpiry = jwtClaims.getExpirationTime();
var now = new Date();
if (jwtIssuer != config.issuer) {
logger.message(config.nodeName + "Issuer in JWT [" + jwtIssuer + "] doesn't match expected issuer [" + config.issuer + "]");
outcome = nodeOutcome.INVALID;
} else if (jwtIssuedAt.after(now)) {
logger.message(config.nodeName + "JWT issued in the future [" + jwtIssuedAt + "]");
outcome = nodeOutcome.INVALID;
} else if (jwtExpiry.before(now)) {
logger.message(config.nodeName + "JWT expired at [" + jwtExpiry + "]");
outcome = nodeOutcome.EXPIRED;
} else {
var objectAttributes = {
"_id" : jwtClaims.getClaim("uid")
};
sharedState.put("objectAttributes",objectAttributes);
sharedState.put("claims", jwtClaims);
outcome = nodeOutcome.TRUE;
}
}
} else {
outcome = nodeOutcome.ERROR;
}