Table of Contents:
- Swagger
- ForgeRock Nodes
- Debuging
- Debug Node
- Debug Script
- RestAPI
Swagger
ForgeRock Swagger
ForgeRock Nodes
NOTE: Bulding a node out of a JavaScript
and importing it into the web-container webapps is apparently faster at rendering than using JS
scripts inside of a journey. From what I have been told. I am still pending the ability to time it out and discover the delay between an imported node from a jar
vs writting out a JavaScript
and referencing it into a journey.
Here are two ways of debugging
Debuging
Debug Node
see: ForgeRock Debug Node
Instructions for installing the node for troubleshooting
:
Debug Authentication Node An authentication node which dumps various properties to debug. Useful for viewing shared state, transient shared state (configurable), request parameters, client details, cookies, etc.
Installation Copy the .jar file from the ../target directory into the ../web-container/webapps/openam/WEB-INF/lib
directory where AM is deployed. Restart the web container to pick up the new node. The node will then appear in the authentication trees components palette.
Usage The node can be dropped into any point in an authentication tree and captures debug data at that point. The node captures: shared state, transient shared state (off by default), request headers, client IP address, request hostname, ssoTokenId (if exists), request cookies, and request query parameters. Note: transient shared state is often used to store sensitive data such as passwords. Enabling this option will cause those values to be written to logs in cleartext. \
To Build Edit the necessary DebugNode.java as appropriate. To rebuild, run “mvn clean install” in the directory containing the pom.xml
Disclaimer The sample code described herein is provided on an “as is” basis, without warranty of any kind.
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2017 ForgeRock AS.
*/
/*
* jon.knight@forgerock.com
*
* Needed to register the node
*/
package org.forgerock.openam.auth.nodes;
import static java.util.Arrays.asList;
import javax.inject.Inject;
import org.forgerock.openam.auth.node.api.AbstractNodeAmPlugin;
import org.forgerock.openam.auth.node.api.Node;
import org.forgerock.openam.plugins.PluginException;
import org.forgerock.openam.sm.AnnotatedServiceRegistry;
import com.iplanet.sso.SSOException;
import com.sun.identity.sm.SMSException;
/**
* Core nodes installed by default with no engine dependencies.
*/
public class DebugNodePlugin extends AbstractNodeAmPlugin {
private final AnnotatedServiceRegistry serviceRegistry;
/**
* DI-enabled constructor.
* @param serviceRegistry A service registry instance.
*/
@Inject
public DebugNodePlugin(AnnotatedServiceRegistry serviceRegistry) {
this.serviceRegistry = serviceRegistry;
}
@Override
public String getPluginVersion() {
return "1.0.0";
}
@Override
public void onStartup() throws PluginException {
for (Class<? extends Node> nodeClass : getNodes()) {
pluginTools.registerAuthNode(nodeClass);
}
}
@Override
protected Iterable<? extends Class<? extends Node>> getNodes() {
return asList(
DebugNode.class
);
}
}
Debug Script
A simple script could be used to capture debug process and output values when troubleshooting. This is also handy and quick way if you do not already have a Debug Node
compiled or if there are some values missing that are needed as the FogeRock Cloud version does have some differences in what the defaults are as to the on-prem version. Even though they are the same code.
1/* AM Journey Template
2 *
3 * Authors: se@forgerock.com
4 *
5 * Description of the script goes here
6 *
7 * This script needs to be parametrized. It will not work properly as is.
8 * It requires some nodes that set at least sharedState before it can operate.
9 * For example, set a page node with Platform Username and Platform Password nodes
10 *
11 * The Scripted Decision Node needs the following outcomes defined:
12 * - true
13 */
14
15// Do everything in a self-invoking function and do not write code outside of a function or you will pay dearly.
16// This is because of top-level scoping/whitelisting/etc issues that give you 'undefined' errors.
17(function () {
18 logger.message("Script: start"); // beging of script main
19 outcome = "true"; // <- fill in default outcome here and it should match a "Script Outcomes" setting on this node itself
20
21 // build output html table that will be sent back to browser
22 var output = createHtml();
23
24 // issue callback to browser after output html is built from createHtml() function
25 displayMessage(output);
26
27 logger.message("Script: end"); // end of script main
28
29 /*
30 * Put functions below here
31 */
32 function createHtml() {
33 var html = "<table class=\"table table-striped\">";
34 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">Shared State Variables (sharedState.get)</th></tr></thead>";
35 // get all the keys in nodeState
36 var iterator = nodeState.keys().iterator();
37 var stateKeys = [];
38 while (iterator.hasNext()) {
39 stateKeys.push(iterator.next().toString());
40 }
41 stateKeys.forEach(function (stateKey) {
42 if (sharedState.get(stateKey)
43 && sharedState.get(stateKey).toString() !== "null"
44 && sharedState.get(stateKey).toString() !== ""
45 && "" + stateKey !== "objectAttributes" // going to pull out objectAttributes later
46 && "" + stateKey !== "pageNodeCallbacks") //pageNodeCallbacks are internal to the Page Node and not needed/used
47 {
48 html += "<tr><td class=\"px-1 py-1\">" + stateKey + "</td><td class=\"px-1 py-1\">" + sharedState.get(stateKey) + "</td></tr>";
49 }
50 });
51 html += "</table>";
52
53 html += "<table class=\"table table-striped\">";
54
55 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">Transient State Variables (transientState.get)</th></tr></thead>";
56 // get all the keys in nodeState
57 var iterator = nodeState.keys().iterator();
58 var stateKeys = [];
59 while (iterator.hasNext()) {
60 stateKeys.push(iterator.next().toString());
61 }
62 stateKeys.forEach(function (stateKey) {
63 if (transientState.get(stateKey)
64 && transientState.get(stateKey).toString() !== "null"
65 && transientState.get(stateKey).toString() !== ""
66 && "" + stateKey !== "objectAttributes") {
67 html += "<tr><td class=\"px-1 py-1\">" + stateKey + "</td><td class=\"px-1 py-1\">" + transientState.get(stateKey) + "</td></tr>";
68 }
69 });
70 html += "</table>";
71
72 html += "<table class=\"table table-striped\">";
73 // Build the table of objectAttributes in sharedState
74 if (sharedState.get("objectAttributes")) {
75 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">Shared Object Attributes (sharedState.get)</th></tr></thead>";
76 var entries = sharedState.get('objectAttributes').entrySet().toArray();
77 entries.forEach(function (entry) { // showing how to use entrySet(). Can use keySet().
78 html += "<tr><td class=\"px-1 py-1\">" + entry.getKey() + "</td><td class=\"px-1 py-1\">" + entry.getValue() + "</td></tr>";
79 });
80 }
81 else {
82 html += "<tr><td colspan=\"2\">EMPTY</td></tr>";
83 }
84 html += "</table>";
85
86 html += "<table class=\"table table-striped\">";
87 // Build the table of objectAttributes in transientState
88 if (transientState.get("objectAttributes")) {
89 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">Transient Object Attributes (transientState.get)</th></tr></thead>";
90 var keys = transientState.get('objectAttributes').keySet().toArray();
91 keys.forEach(function (key) { // showing how to use keySet(). Can use entrySet().
92 html += "<tr><td class=\"px-1 py-1\">" + key + "</td><td class=\"px-1 py-1\">" + transientState.get('objectAttributes').get(key) + "</td></tr>";
93 });
94 }
95 else {
96 html += "<tr><td colspan=\"2\">EMPTY</td></tr>";
97 }
98 html += "</table>";
99
100 html += "<table class=\"table table-striped\">";
101 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">nodeState.get (transientState, secureState, sharedState)</th></tr></thead>";
102 // get all the keys in nodeState
103 var iterator = nodeState.keys().iterator();
104 var stateKeys = [];
105 while (iterator.hasNext()) {
106 stateKeys.push(iterator.next().toString());
107 }
108 stateKeys.forEach(function (stateKey) {
109 if (nodeState.get(stateKey)
110 && nodeState.get(stateKey).toString() !== "null"
111 && nodeState.get(stateKey).toString() !== ""
112 && "" + stateKey !== "pageNodeCallbacks") //pageNodeCallbacks are internal to the Page Node and not needed/used
113
114 {
115 html += "<tr><td class=\"px-1 py-1\">" + stateKey + "</td><td class=\"px-1 py-1\">" + nodeState.get(stateKey) + "</td></tr>";
116 }
117 });
118 html += "</table>";
119
120
121 html += "<table class=\"table table-striped\">";
122 // looking for a way to build this AM User Profile list dynamically
123 var objAMAttrs = [
124 "uid",
125 "cn",
126 "inetUserStatus",
127 "givenName",
128 "sn",
129 "mail",
130 "description",
131 "telephoneNumber",
132 "street",
133 "l",
134 "postalCode",
135 "co",
136 "st",
137 "displayName",
138 "fr-attr-istr1",
139 "fr-attr-istr2",
140 "fr-attr-istr3",
141 "fr-attr-istr4",
142 "fr-attr-istr5",
143 "fr-attr-str1",
144 "fr-attr-str2",
145 "fr-attr-str3",
146 "fr-attr-str4",
147 "fr-attr-str5",
148 "fr-attr-imulti1",
149 "fr-attr-imulti2",
150 "fr-attr-imulti3",
151 "fr-attr-imulti4",
152 "fr-attr-imulti5",
153 "fr-attr-multi1",
154 "fr-attr-multi2",
155 "fr-attr-multi3",
156 "fr-attr-multi4",
157 "fr-attr-multi5",
158 "fr-attr-idate1",
159 "fr-attr-idate2",
160 "fr-attr-idate3",
161 "fr-attr-idate4",
162 "fr-attr-idate5",
163 "fr-attr-date1",
164 "fr-attr-date2",
165 "fr-attr-date3",
166 "fr-attr-date4",
167 "fr-attr-date5",
168 "fr-attr-iint1",
169 "fr-attr-iint2",
170 "fr-attr-iint3",
171 "fr-attr-iint4",
172 "fr-attr-iint5",
173 "fr-attr-int1",
174 "fr-attr-int2",
175 "fr-attr-int3",
176 "fr-attr-int4",
177 "fr-attr-int5"
178 ];
179
180 // Build the table of idRepository binding
181 var attrs2;
182 if (sharedState.get("_id") && idRepository.getAttribute(sharedState.get("_id"), "uid")) {
183 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">idRepository AM User Profile</th></tr></thead>";
184 var id = sharedState.get("_id");
185 objAMAttrs.forEach(function (attr) {
186 attrs = idRepository.getAttribute(id, attr);
187 if (attrs && "" + attrs !== "null" && "" + attrs !== "" && "" + attrs.size() > 0) {
188 if (attrs.size() === 1) {
189 attrs = singleValue(attrs);
190 }
191 html += "<tr><td class=\"px-1 py-1\">" + attr + "</td><td class=\"px-1 py-1\">" + attrs + "</td></tr>";
192 }
193 });
194 }
195 html += "</table>";
196
197 html += "<table class=\"table table-striped\">";
198 html += "<thead class=\"thead-dark\"><tr><th class=\"px-1 py-1\" colspan=\"2\">Request Headers</th></tr></thead>";
199 //html += "<tr><td colspan=\"2\">" + requestHeaders.toString() + "</td></tr>";
200 var rHeaders = String(requestHeaders).split('], ').map(function (header) {
201 return header.split('=')[0].replace('{', '').replace('}', '');
202 });
203 rHeaders.forEach(function (headerName) {
204 var header = requestHeaders.get(headerName);
205 html += "<tr><td class=\"px-1 py-1\">" + headerName + "</td><td class=\"px-1 py-1\">" + header.get(0) + "</td></tr>";
206 });
207
208 html += "</table>";
209
210 return html;
211 }
212
213 //builds the html to display the message in the browser on the callback
214 //use view source in browser and look for class="callback-component" to see html response
215 function displayMessage(message) {
216 var anchor = "anchor-".concat(generateNumericToken('xxx'));
217 var halign = "left";
218 var script = "Array.prototype.slice.call(\n".concat(
219 "document.getElementsByClassName('callback-component')).forEach(\n").concat(
220 "function (e) {\n").concat(
221 " var message = e.firstElementChild;\n").concat(
222 " if (message.firstChild && message.firstChild.nodeName == '#text' && message.firstChild.nodeValue.trim() == '").concat(anchor).concat("') {\n").concat(
223 " message.className = \"\";\n").concat(
224 " message.style = \"\";\n").concat(
225 " message.align = \"").concat(halign).concat("\";\n").concat(
226 " message.innerHTML = '").concat(message).concat("';\n").concat(
227 " }\n").concat(
228 "})")
229 var fr = JavaImporter(
230 org.forgerock.openam.auth.node.api.Action,
231 javax.security.auth.callback.TextOutputCallback,
232 com.sun.identity.authentication.callbacks.ScriptTextOutputCallback
233 )
234 if (message.length && callbacks.isEmpty()) {
235 action = fr.Action.send(
236 new fr.TextOutputCallback(
237 fr.TextOutputCallback.INFORMATION,
238 anchor
239 ),
240 new fr.ScriptTextOutputCallback(script)
241 ).build()
242 }
243 else {
244 action = fr.Action.goTo(outcome).build();
245 }
246 }
247
248 /*
249 * Generate a token in the desired format. All 'x' characters will be replaced with a random number 0-9.
250 * This is needed to have a unique div(anchor-x) on the html callback that we can populate data
251 * Example:
252 * 'xxxxx' produces '28535'
253 * 'xxx-xxx' produces '432-521'
254 */
255 function generateNumericToken(format) {
256 return format.replace(/[x]/g, function (c) {
257 var r = Math.random() * 10 | 0;
258 var v = r;
259 return v.toString(10);
260 });
261 }
262
263 // get a singleValue from a HashSet
264 function singleValue(x) {
265 if (x.size() > 0) {
266 return x.iterator().next();
267 }
268
269 return "";
270
271 }
272
273}()); // self-invoking function
RestAPI
Similar to Cloud service accounts => https://drive.google.com/file/d/10yxqUBwWGzLXO3aV8hKBn7XqicXgchb9/view
$ curl --location 'https://forgerock-example.com:8443/am/oauth2//access_token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic ZmRjZmI2YWQtNzJhOC00M2FjLTgyMmItZTViNmRkZWM3YjIxOkZyZHAtMjAxMA==' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EzYwobpSWRQhB1slQPChxh3kyj4.*AAJTSQACMDEAAlNLABxZSDFGai9vRDgyOWdBaTFxV0t1OHdwVnlRWFU9AAR0eXBlAANDVFMAAlMxAAA.*' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=fr:idm:*’
$ curl --location 'https://forgerock-example.com:9443/openidm/managed/user?_queryFilter=true&_fields=*' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIoYWdlIWZkY2ZiNmFkLTcyYTgtNDNhYy04MjJiLWU1YjZkZGVjN2IyMSkiLCJjdHMiOiJPQVVUSDJfU1RBVEVMRVNTX0dSQU5UIiwiYXVkaXRUcmFja2luZ0lkIjoiNjkwYjMyZmUtZmZlMi00MWQ1LTllMjgtOWIyY2Q5Njk3OTYzLTI5NzE3Iiwic3VibmFtZSI6ImZkY2ZiNmFkLTcyYTgtNDNhYy04MjJiLWU1YjZkZGVjN2IyMSIsImlzcyI6Imh0dHBzOi8vc25vdy5jdXppY2FuLmlvOjg0NDMvYW0vb2F1dGgyIiwidG9rZW5OYW1lIjoiYWNjZXNzX3Rva2VuIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImF1dGhHcmFudElkIjoiVjc2ODA0NDZBR0ZCTlN6Y0hjVEFubTlKTHl3IiwiYXVkIjoiZmRjZmI2YWQtNzJhOC00M2FjLTgyMmItZTViNmRkZWM3YjIxIiwibmJmIjoxNzE3NTI5NTc3LCJncmFudF90eXBlIjoiY2xpZW50X2NyZWRlbnRpYWxzIiwic2NvcGUiOlsiZnI6aWRtOioiXSwiYXV0aF90aW1lIjoxNzE3NTI5NTc3LCJyZWFsbSI6Ii8iLCJleHAiOjE3MTc1MzMxNzcsImlhdCI6MTcxNzUyOTU3NywiZXhwaXJlc19pbiI6MzYwMCwianRpIjoidjJnMDFGTkNsRkR0dzlJUEFFRU5kOE5SMjc4In0.JEwzEGEUfP0UQK8JLisXMhKVE94Fz5gACo8q3KxGmhs' \
--header 'Cookie: amlbcookie=01; iPlanetDirectoryPro=EzYwobpSWRQhB1slQPChxh3kyj4.*AAJTSQACMDEAAlNLABxZSDFGai9vRDgyOWdBaTFxV0t1OHdwVnlRWFU9AAR0eXBlAANDVFMAAlMxAAA.*’
$ cat authentication.json
// 20240403160537
// https://backstage.forgerock.com/docs/platform/7.5/_attachments/authentication.json
{
"rsFilter": {
"clientId": "idm-resource-server",
"clientSecret": "&{rs.client.secret|password}",
"tokenIntrospectUrl": "https://forgerock-example.com:8443/am/oauth2/introspect",
"scopes": [
"fr:idm:*"
],
"cache": {
"maxTimeout": "300 seconds"
},
"augmentSecurityContext": {
"type": "text/javascript",
"source": "require('auth/orgPrivileges').assignPrivilegesToUser(resource, security, properties, subjectMapping, privileges, 'privileges', 'privilegeAssignments');"
},
"subjectMapping": [
{
"resourceTypeMapping": {
"usr": "managed/user",
"age": "managed/user"
},
"propertyMapping": {
"sub": "_id"
},
"userRoles": "authzRoles/*",
"additionalUserFields": [
"adminOfOrg",
"ownerOfOrg"
],
"defaultRoles": [
"internal/role/openidm-authorized"
]
}
],
"staticUserMapping": [
{
"subject": "(usr!amAdmin)",
"localUser": "internal/user/openidm-admin",
"roles": [
"internal/role/openidm-authorized",
"internal/role/openidm-admin"
]
},
{
"subject": "(age!idm-provisioning)",
"localUser": "internal/user/idm-provisioning",
"roles": [
"internal/role/platform-provisioning"
]
}
],
"anonymousUserMapping": {
"localUser": "internal/user/anonymous",
"roles": [
"internal/role/openidm-reg"
]
}
}
}