Skip to main content
CodingIdentity Access Management

How to Auto-Create Identities from SAML Assertion with ForgeRock AM and IDM

By January 6, 2022May 19th, 2023No Comments

In this writeup, we will discuss how to perform custom SAML Assertion processing by writing a custom Java class for use with Federation in ForgeRock AM. In this example, a Ping Federate IdP is set up with federation with ForgeRock as the SP. The idea in this instance was to intercept the SAML assertion from an IdP initiated SSO process so that the contents could be passed on to ForgeRock IDM to provision the user account. This was done by downloading the ForgeRock AM source code and writing a custom Java class that overrides a method in the DefaultLibrarySPAccountMapper class which AM uses during the mapping process.

High Level Overview

During the federation process, when AM receives a SAML assertion from an IdP, it then uses the contents of the assertion to map them to an account in the directory. By default, AM uses an instance of com.sun.identity.saml2.plugins.DefaultSPAccountMapper as the mapper class when processing the SAML assertion. This is fine if you’re OK with the mappings defined in the XML configurations shared between the IdP/SP, but if you want anything custom you’ll have to look elsewhere. One option is using Authentication Trees, where you can design a custom authentication flow based around decision nodes. AM offers a SAML2 Authentication node, where you can tie the node to an already set up federation configuration, and choose what happens when an account exists or doesn’t exist, meaning it’s possible to tie execution to a scripted decision node if the account does not exist, meaning you could pull metadata from the SAML assertion to manipulate as you please, as in the following screenshot:

The problem with this approach is that AM only supports this with SP initiated SSO, which will not work here. For our purposes, writing a custom Java class is the best approach.

Writing the Code

The first step is to obtain the source code for AM as described in the following article: https://backstage.forgerock.com/knowledge/kb/article/a80843067. After your repository is configured, start a clean Maven project. Open pom.xml and add the following three entries to the dependencies:

xml <dependency> <groupId>org.forgerock.am</groupId> <artifactId>openam-federation-library</artifactId> <version>7.0.1</version> </dependency> <dependency> <groupId>com.google.http-client</groupId> <artifactId>google-http-client</artifactId> </dependency> <dependency> <groupId>com.google.cloud</groupId> <artifactId>libraries-bom</artifactId> <version>24.1.1</version> <type>pom</type> <scope>import</scope> </dependency> openam-federation-library

contains the libraries that we need for our class. google-http-client is a simple HTTP request library that we’re going to use to make a REST call to IDM for provisioning the account.

Then, in App.java, you can start writing the custom class. The main objective here is to subclass the DefaultLibarrySPAccountMapper class and override the getIdentity method, which exists within the com.sun.identity.saml2.plugins package. Here’s our example subclass:

package com.psyance;
import java.io.*;
import java.util.*;
import com.sun.identity.saml2.plugins.*;
import com.sun.identity.saml2.assertion.Assertion;
import com.sun.identity.saml2.assertion.AttributeStatement;

import com.sun.identity.saml2.common.SAML2Exception;
import com.google.api.client.http.*;
import com.google.api.client.http.javanet.NetHttpTransport;


public class App extends DefaultLibrarySPAccountMapper
{
    
    @Override
    public String getIdentity(Assertion assertion, String hostEntityID, String realm) throws SAML2Exception {
       
        List l = assertion.getAttributeStatements();
      
        String uid = l.get(0).getAttribute().get(0).getAttributeValueString().get(0);
        String mail = l.get(0).getAttribute().get(1).getAttributeValueString().get(0);
        String givenName = l.get(0).getAttribute().get(2).getAttributeValueString().get(0);
        String sn = l.get(0).getAttribute().get(3).getAttributeValueString().get(0);
        System.out.println(uid + " " + mail + " " + givenName + " " + sn);
        return createUserInIdm(uid, mail, givenName, sn);
    }
    
    private String createUserInIdm(String uid, String mail, String givenName, String sn) {
        HttpRequestFactory rf = new NetHttpTransport().createRequestFactory();
        try {
            String body = String.format("{\"userName\": \"%s\", \"mail\": \"%s\", \"givenName\": \"%s\", \"sn\": \"%s\"}", uid, mail, givenName, sn);
            HttpRequest post = rf.buildPostRequest(new GenericUrl("http://localhost:7080/openidm/managed/user?_action=CREATE"), ByteArrayContent.fromString(null, body));
            HttpHeaders h = new HttpHeaders();
            h.put("X-OpenIDM-Username", "openidm-admin");
            h.put("X-OpenIDM-Password", "openidm-admin");
            h.setContentType("application/json");
            post.setHeaders(h);
            String resp = post.execute().parseAsString();
            System.out.println(resp);
            return uid;
        } catch (IOException e) {
            e.printStackTrace(System.out);
        }
        return "failure";
    }
 }

Inside getIdentity, we’re pulling out the attributes from the SAML assertion, and storing them in local variables. Then we’re calling createUserInIdm, which then makes a REST call to IDM to provision the account. To compile and deploy this code, run mvn clean package in the Maven project’s directory. Then inside the target subdirectory, you’ll see an output JAR file, e.g. java-project-1.0-SNAPSHOT.jar. You’ll need to copy that in to the Tomcat webapps directory where you’ve deployed AM. Stop Tomcat, then copy the JAR file to /path/to/tomcat/webapps/name_of_am_deployment/WEB-INF/lib. Then, restart Tomcat, and go to the configuration of the hosted SP under Applications –> Federation –> Entity Provider –> (your hosted SP entity). Then go to Assertion Processing, and put in the fully-qualified class name of your custom class in the JAR file that you copied, as in this screenshot:

Next time someone begins IDP-initiated SSO, the custom mapper will be called. The calls to IDM and the data pulled from the assertion will be dumped to catalina.out inside /path/to/tomcat9/logs, so you can tail this file to see the assertion processing in action.

Demo

  1. We browse to the IdP SSO page, in this case Ping:

2. After logging in, Ping passes back the SAML assertion, which the browser forwards to AM.

3. When AM receives the assertion, it sees that the user does not exist, and calls the custom mapper, which calls IDM and provisions the account:

4. After the user is provisioned, the mapper returns the user ID back to AM, which now sees that the user exists in the directory, and authenticates the user based on the SAML assertion.

5. The user is logged in and able to access a protected resource.

6. Looking in the AM web console, the user has an account in the directory.

If the user goes to try to log in a second time, the mapper is not called because the account already exists, and the user is automatically authenticated.