We have a Keycloak Script Mapper to add attributes of roles to the ID token. The goal is to aggregate the values available in attributes for the roles. The mapper looks like this:
/**
* Merge with concatenation for the values of attributes obtained
* from the token with the attributes obtained from the roles. If for
* example a group and a role have the same attribute key, the values
* for that key from the group and role will be concatenated.
*
* Known limitations:
* When only roles have a certain attribute, and not a group, the
* mapper only uses the first role it can find. Bug was difficult to
* fix because state in the variable currentClaims seems to be
* persisted over multiple calls.
* Workaround: Also add this specific attribute to a group with a
* dummy value.
*
* NOTE: there is no role attribute mapper out-of-the-box in
* Keycloak.
*
* Available variables in script:
* user - the current user
* realm - the current realm
* token - the current token
* userSession - the current userSession
* keycloakSession - the current keycloakSession
*
* Documentation on available variables:
* https://stackoverflow.com/a/52984849
*/
var currentClaims = {};
token.getOtherClaims().forEach(function(k, v) {
currentClaims[k] = v;
});
function isMultiValued(v) {
// From experience, multivalued attribute values are sometimes
// Arrays and sometimes Objects. Thus look for negative case:
// anything other than a string is multivalued.
return !(typeof v === 'string' || v instanceof String);
}
function addToList(l, values) {
for each(var v in values) {
l.add(v);
}
return l;
}
function toStringArray(arr) {
return Java.to(arr, "java.lang.String[]");
}
user.getRealmRoleMappings().forEach(function(roleModel) {
roleModel.getAttributes().forEach(function(k, v) {
var currentValue = currentClaims[k];
if (k in currentClaims) {
if (!isMultiValued(currentValue)) {
v = toStringArray([currentValue].concat(v));
} else {
v = addToList(currentValue, v);
}
}
currentClaims[k] = v; // <= to also aggregate over roles!
token.setOtherClaims(k, v);
});
});
The part with currentClaims[k] = v
I added to aggregate values available in roles, so that also if two roles contain the same attribute their values are aggregated.
E.g., if we have a user has the roles a and b with respectively attribute foo with value 1 and 2, we expect that the ID token contains a claim for foo with the values 1 and 2.
user:
role a foo -> 1
role b foo -> 2
expected ID token:
foo -> [1, 2]
But with the current code the currentClaims
variable seems to hold state over multiple calls to the function. Every time the ID token is inspected more values for 2
are added into the token, leading to a foo claim like [1, 2, 2, ..., 2]
with more and more 2
s added every time the token is retrieved. I tried wrapping the whole call in a function so that possibly state was discarded between calls, but to no avail. This is the result:
Why is the state kept over multiple calls? And is there a way to also aggregate the values for the role attributes?