Adding IDM roles to access/ID token - solution, but is there an easier way?

I have a requirement to add a user’s roles (provisioning roles) as a custom claim to the ID token or access token. I have implemented this via a custom OIDC claims script, but it was really much more involved than I would have thought. So before I submit this as the preferred solution, I’d like to check that I’m not missing a simpler method.

The main issue seems to be that the OAuth2 customization script environments are all lacking the ‘openidm’ binding. I can lookup the UUIDs of the roles using eg the fr-idm-effectiveRole attribute, but to get the name of the role it seems like you have to call out to IDM to get it. That involves a lot of steps due to the lack of a binding in these scripts:

  1. Create a service account and grant it the fr:idm:* permission.
  2. Create an ESV variable for the SA id and a secret for the SA private key JWK.
  3. Load those ESVs in the script and use the FR JOSE library to parse the JWK private key.
  4. Create a signed JWT using the private key and call AM using the jwt-bearer grant type to obtain an access token.
  5. Use the httpClient to call IDM’s managed object endpoint using the access token.

This ends up being quite a lot of code! (I’d also feel happier if I could restrict that service account to read-only perms on managed users/roles).

I can share code if anyone else is solving the same problem. So, does anyone know a simpler way?

In case anyone is interested, this is the core part of the code I am currently using (some imports and other details omitted):

        function getIdmAccessToken() {
          logger.message('Generating JWT to obtain IDM access token');
          var idmServiceAccountID  = systemEnv.getProperty('esv.idm.script.service.account.id');
          var idmServiceAccountKey = systemEnv.getProperty('esv.idm.script.client.private.key');
          if (!idmServiceAccountKey) {
            logger.error('Unable to get IDM Service Account private key');
            return;
          }
          var amHost = requestProperties.get('requestUri').getHost();
          var endpoint = 'https://' + amHost + ':443/am/oauth2/access_token';
          var jwk = frJava.RsaJWK.parse(idmServiceAccountKey);
          var handler = new frJava.SigningManager().newSigningHandler(jwk);
          var jwtBuilder = new frJava.JwtBuilderFactory();
          var claims = new frJava.JwtClaimsSet();
          claims.setIssuer(idmServiceAccountID);
          claims.setSubject(idmServiceAccountID);
          claims.addAudience(endpoint);
          claims.setClaim("exp", new Date().getTime() / 1000 + 180);
          claims.setJwtId(java.util.UUID.randomUUID().toString());
          var jwt = jwtBuilder.jws(handler)
                  .headers().header("alg", "RS256").done()
                  .claims(claims)
          		  .build();
          logger.message('Generated JWT: {}...', jwt.substring(0, 20));
          
          var request = new http.Request()
          	.setUri(endpoint)
          	.setMethod('POST');
          var body = new http.Form();
          body.add('client_id', 'service-account');
          body.add('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
          body.add('assertion', jwt);
          body.add('scope', 'fr:idm:*');
          request.setEntity(body);
          
          var response = httpClient.send(request).get();
          logger.message('Authentication response: {}', response.getStatus());
          if (response.getStatus().getCode() !== 200) {
            return;
          }
          var json = JSON.parse(response.getEntity().getString());
          var accessToken = json.access_token;
          logger.message('Got access token: {}...', accessToken.substring(0, 5));
          return accessToken;
        }
      
        var cachedUserData;
        function loadUserData(fields) {
          if (cachedUserData) {
            return cachedUserData;
          }
          if (identity) {
              var token = getIdmAccessToken();
              var uuid = identity.getAttribute('fr-idm-uuid').toArray()[0];
              var realm = requestProperties.get('realm');
              
              var request = new http.Request()
          			.setMethod('GET')
          			.setUri('http://idm/openidm/managed' + realm + '_user/' + uuid + 
                            '?_fields=' + fields);
          	  request.getHeaders().add('Authorization', 'Bearer ' + token);
         	  request.getHeaders().add('Accept-API-Version', 'resource=1.0,protocol=1.0');
          	  var response = httpClient.send(request).get();
          	  logger.message('Called IDM to get user information: {}', response.getStatus());
          	  if (response.getStatus().getCode() !== 200) {
                logger.error('Bad response: {}', response.getEntity().getString());
            	return;
          	  }
              cachedUserData = JSON.parse(response.getEntity().getString());
              logger.message('User data: {}', JSON.stringify(cachedUserData));
           }
           return cachedUserData;
        }
      
        function getUserRoles() {
          function resolveClaim(claim) {
            var roles = [];
            var data = loadUserData('roles/*_ref/name,effectiveAssignments/*_ref/name');
              
            for (var i = 0; i < data.roles.length; ++i) {
              roles.push(data.roles[i].name);
            }
           
            return roles;
          }
          return resolveClaim;
        }