PingGateway

The FAPI trusted directory

In a FAPI-based ecosystem, a central authority operates a trusted directory. Although not required for FAPI conformance testing, the trusted directory is a critical component of the ecosystem.

The trusted directory records the keys and other metadata for API clients of trusted organizations. The central authority vets the participants and restricts access to trusted organizations and their API clients. Each trusted organization registers its API clients with the trusted directory before registering them with other organizations.

When an API client registers with another organization, the trusted directory ensures only a trusted API clients can do so:

  • Each organization configures their authorization server to use the trusted directory.

    When an API client registers dynamically with the authorization server, it presents a signed software statement assertion (SSA) from the trusted directory.

    The authorization server validates the software statement including the signature and issuer.

  • The software statement from the trusted directory includes the client’s keys and other metadata. It vouches for the API client as a trusted participant in the ecosystem.

    The trusted directory issues software statement assertions only to trusted API clients of trusted organizations.

  • The trusted directory can revoke an API client’s participation in the ecosystem.

Production deployments in FAPI-based ecosystems require the trusted directory. In each participating organization, the authorization server depends on SSAs from the central trusted directory. PingGateway protects dynamic client registration (DCR) requests to the authorization server, allowing only trusted API clients to register.

Make sure you understand the trusted directory’s role in API client registration. This page explains how to use the test trusted directory for this tutorial.

Run the test trusted directory

The test trusted directory is a Docker image available as gcr.io/forgerock-io/ig/demo-fapi-trusted-directory:2025.9. You can run it locally or in a test environment accessible to PingGateway.

The test trusted directory requires the following:

  • A fully qualified domain name (FQDN).

    If you’re running the test trusted directory locally, add an alias to your hosts file:

    127.0.0.1	trustdir.example.com
  • CA keys for TLS and another key pair for signing. Generate your own for testing.

    When you run the Docker container, mount the keystore for the keys under /var/ig/ in the container.

The following sample script runs the test trusted directory as a Docker container listening on port 9080. It generates the keys in a ./secrets folder using the keytool command on the first run:

#!/usr/bin/env bash

DOCKER_IMAGE="gcr.io/forgerock-io/ig/demo-fapi-trusted-directory:2025.9"

checkForDocker() {
  OUT=$(docker --version)
  RC=$?
  if [ $RC -ne 0 ]; then
      echo "### 'docker' command not found. The test trusted directory requires Docker to run."
      exit $RC
  else
    echo "Found ${OUT}"
  fi
}

checkForKeytool() {
  OUT=$(keytool --version)
  RC=$?
  if [ $RC -ne 0 ]; then
      echo "### 'keytool' command not found. The test trusted directory requires keytool to create test keys."
      exit $RC
  else
    echo "Found ${OUT}"
  fi
}

createKeys() {
  echo "### Creating trusted directory keystore ${PWD}/secrets/test-trusted-dir.p12..."
  echo
  mkdir -p secrets

  echo "### Generating CA keys..."
  echo
  keytool -keystore secrets/test-trusted-dir.p12 -storetype PKCS12 \
          -genkeypair -alias ca -dname "CN=Test Trusted Directory Root CA" \
          -ext bc=ca:true -keyalg RSA -keysize 2048 -validity 3650 \
          -storepass changeit -keypass changeit

  echo
  echo "### Generating JWT Signing keys..."
  echo
  keytool -keystore secrets/test-trusted-dir.p12 -storetype PKCS12 \
          -genkeypair -alias jwt-signer -dname "CN=Test Trusted Directory JWT Signer" \
          -keyalg RSA -keysize 2048 -validity 3650 \
          -storepass changeit -keypass changeit

  echo
  echo "### Listing keystore contents..."
  echo
  keytool -keystore secrets/test-trusted-dir.p12 -storetype PKCS12 \
          -list -storepass changeit

  echo
  echo "### Exporting CA certificate..."
  echo
  keytool -keystore secrets/test-trusted-dir.p12 -storetype PKCS12 \
          -exportcert -alias ca -rfc -file secrets/test-trusted-dir-ca.pem -storepass changeit

  echo
  echo "### Done."
}

runTestTrustedDirectory() {
  echo "### Running test trusted directory in Docker..."
  echo

  docker pull "${DOCKER_IMAGE}"

  docker run \
  --detach \
  --init \
  --tty \
  --rm \
  --env IG_METRICS_PASSWORD=password \
  --env IG_METRICS_USERNAME=user \
  --env IG_TEST_DIRECTORY_CA_KEYSTORE_ALIAS=ca \
  --env IG_TEST_DIRECTORY_CA_KEYSTORE_KEYPASS=changeit \
  --env IG_TEST_DIRECTORY_CA_KEYSTORE_PATH=/secrets/test-trusted-dir.p12 \
  --env IG_TEST_DIRECTORY_CA_KEYSTORE_STOREPASS=changeit \
  --env IG_TEST_DIRECTORY_CA_KEYSTORE_TYPE=PKCS12 \
  --env IG_TEST_DIRECTORY_FQDN=trustdir.example.com \
  --env IG_TEST_DIRECTORY_ISSUER_NAME="FAPI Test Trusted Directory" \
  --env IG_TEST_DIRECTORY_SIGNING_KEYSTORE_ALIAS=jwt-signer \
  --env IG_TEST_DIRECTORY_SIGNING_KEYSTORE_KEYPASS=changeit \
  --env IG_TEST_DIRECTORY_SIGNING_KEYSTORE_PATH=/secrets/test-trusted-dir.p12 \
  --env IG_TEST_DIRECTORY_SIGNING_KEYSTORE_STOREPASS=changeit \
  --env IG_TEST_DIRECTORY_SIGNING_KEYSTORE_TYPE=PKCS12 \
  --mount type=bind,source="${PWD}/secrets",target=/var/ig/secrets,readonly \
  --name test-trusted-directory \
  --publish 9080:8080 \
  "${DOCKER_IMAGE}"
}

checkForDocker

if [ ! -f ./secrets/test-trusted-dir.p12 ]; then
  checkForKeytool
  createKeys
else
  echo "### Trusted directory keystore already exists, skipping key creation."
  echo
fi

runTestTrustedDirectory

Notice the issuer name is FAPI Test Trusted Directory. When the test trusted directory issues a software statement, it uses this issuer name. You’ll need it when you register the trusted directory in PingOne Advanced Identity Cloud.

You’ve successfully started the test trusted directory.

Register the test trusted directory

In PingOne Advanced Identity Cloud the trusted directory plays the role of a software publisher. The authorization server uses a software publisher agent profile with the trusted directory’s metadata and keys. It uses the profile to validate and trust software statements (SSAs) the API clients present during registration.

In production deployments, use your FAPI ecosystem’s official trusted directory service instead.
  1. Get the test trusted directory keys:

    $ curl http://trustdir.example.com:9080/jwkms/testdirectory/jwks

    The trusted directory responds with the JSON Web Key Set (JWKs) containing its public keys.

  2. In the Advanced Identity Cloud admin UI, click open_in_new Native Consoles > Access Management to open the AM admin UI.

  3. Go to Services > Applications > Software Publisher > Add Software Publisher Agent and configure the account for the trusted directory:

    Field Value

    Agent ID

    Trusted Directory

    Software publisher issuer

    FAPI Test Trusted Directory

    Software statement signing Algorithm

    PS256

    Public key selector

    JWKs

    Json Web Key

    The trusted directory JWKs.

  4. Click Save Changes.

You have successfully configured the software publisher account for the test trusted directory.

Register API clients

The FAPI conformance tests use two API clients, each with different keys and the same redirect URIs.

Register both API clients using the test trusted directory by following these steps for each client:

Get the keys

The test trusted directory issues keys directly for tests API clients. In a production deployment, the organization generates the keys and the trusted directory signs the certificate.

  1. Get the keys for the API client from the trusted directory.

    The request specifies the organization ID and name and a software ID for the API client as in the following example:

    $ curl \
    --request POST \
    --url http://trustdir.example.com:9080/jwkms/apiclient/issuecert \
    --header 'Content-Type: application/json' \
    --data '{
        "org_id": "da968939-e034-4c02-8b47-29ea95d3ecbf",
        "org_name": "Test organization",
        "software_id": "test-client-1"
    }'
  2. Keep a copy of the response for use when requesting the SSA.

    The trusted directory responds with the API client’s keys as a JWK set. The JWK set includes two key pairs:

    • A key pair for transport-level security (TLS).

    • A key pair for signing JWTs.

  3. Get the PEM-format TLS certificate and private key from the JWK set using the test trusted directory.

    Use a command such as the following example. The POST data consists of a JWK set containing only the TLS key object from the previous step:

    $ curl \
    --location \
    --globoff \
    --request POST \
    --url http://trustdir.example.com:9080/jwkms/apiclient/gettlscert \
    --header 'Content-Type: application/json' \
    --data '{
        "keys": [
            {
                "...": "...",
                "use": "tls",
                "...": "..."
            }
        ]
    }'

    The test trusted directory returns two PEM-format fields, one for the certificate, the other for the private key. Save both for each client to use when configuring conformance tests.

Get the software statement

The trusted directory issues a software statement assertion (SSA) only to an API client with the keys it issued. To get the SSA, you need the keys to authenticate the request and to fulfill the SSA claims.

  1. Transform the TLS certificate to a PEM-format header for the SSA request.

    The test trusted directory requires the TLS certificate in a ssl-client-cert header in the request to get the SSA.

    1. Extract the tls certificate from the first x5c field of the key.

    2. Wrap it with -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- on their own lines.

    3. URL-encode the result.

    Use any tool you like to prepare the header value. The following example Groovy script uses the extracted <transport-cert> value:

    def x5c = "<transport-cert>"
    def cert = """-----BEGIN CERTIFICATE-----
    ${x5c}
    -----END CERTIFICATE-----"""
    
    print java.net.URLEncoder.encode(cert, "UTF-8")
  2. Set the header value as an environment variable named SSL_CLIENT_CERT.

    $ export SSL_CLIENT_CERT='-----BEGIN+CERTIFICATE-----<url-encoded-transport-cert>-----END+CERTIFICATE-----'
  3. Get the SSA.

    The following example uses the ssl-client-cert header to authenticate the request and a software statement body with the keys issued to the API client. The response is valid for only 5 minutes, so use this step immediately before registering the API client:

    $ curl \
    --request POST \
    --url http://trustdir.example.com:9080/jwkms/apiclient/getssa \
    --header 'Content-Type: application/json' \
    --header "ssl-client-cert: ${SSL_CLIENT_CERT}" \
    --data '{
        "software_id": "test-client-1",
        "software_client_name": "FAPI test client 1",
        "software_client_id": "dd44cb4f-173b-4d0f-bb97-b21f4238bbda",
        "software_tos_uri": "https://example.com/terms-of-service",
        "software_client_description": "FAPI test client for conformance tests",
        "software_redirect_uris": [
            "https://www.certification.openid.net/test/a/pinggateway/callback",
            "https://www.certification.openid.net/test/a/pinggateway/callback?dummy1=lorem&dummy2=ipsum"
        ],
        "software_policy_uri": "https://example.com/policy",
        "software_logo_uri": "https://example.com/logo.png",
        "software_roles": [
            "DATA"
        ],
        "software_jwks": <api-client-jwk>,
    }'

    Notice the following aspects of the payload:

    • The software_id claim matches the ID you used when requesting the keys.

    • The software_client_id claim is unique.

    • The software_redirect_uris claim includes the URIs required for the FAPI conformance tests.

      Change pinggateway in the URLs to a path element that’s unique to your test deployment.

    • The software_jwks field contains the JWK set you received when requesting the keys.

    • The other claims depend on your API client and FAPI ecosystem.

    The trusted directory responds with the SSA as a JWT. Use it within 5 minutes to register the API client.

Register the API client

Register the client through PingGateway into PingOne Advanced Identity Cloud.

You provide the SSA and other client metadata as a client-signed JWT in the POST data of the registration request. Make sure you use the SSA and your client-signed JWT before either expires.

  1. Prepare the JSON payload for the client registration request.

    Here’s an example payload template for the first API client:

    {
      "client_name": "FAPI test client 1",
      "issuer": "FAPI Test Trusted Directory",
      "grant_types": [
        "authorization_code",
        "client_credentials",
        "refresh_token"
      ],
      "id_token_signed_response_alg": "PS256",
      "iss": "test-client-1",
      "redirect_uris": [
        "https://www.certification.openid.net/test/a/pinggateway/callback",
        "https://www.certification.openid.net/test/a/pinggateway/callback?dummy1=lorem&dummy2=ipsum"
      ],
      "request_object_signing_alg": "PS256",
      "response_types": [
        "code id_token"
      ],
      "scope": "openid",
      "token_endpoint_auth_method": "tls_client_auth",
      "tls_client_auth_subject_dn": "CN=Test organization",
      "token_endpoint_auth_signing_alg": "PS256",
      "exp": <expiration-time>,
      "iat": <issued-at-time>,
      "nbf": <issued-at-time>,
      "software_statement": "<SSA-JWT>"
    }
    1. Copy the SSA JWT into the template as the software_statement value.

    2. Decode the SSA JWT using the Ping Identity JWT Decoder online.

    3. Copy the exp and iat claim values from the decoded SSA JWT into the template.

      Use the value of iat to set the nbf (not before) claim.

  2. Generate the signed JWT for the client registration request:

    1. Go to https://www.jwt.io/, select JWT encoder, and fill the form:

      Field Use

      Header

      { "alg": "PS256", "typ": "JWT" }

      Payload

      The completed JSON template from the previous step.

      Sign JWT

      Find the sig key in the JWK set you received when requesting the keys from the trusted directory.

      Copy and paste the JSON object just for that key, not the whole JWK set.

      Private key format

      JWK

  3. Copy the resulting signed JWT to your clipboard.

  4. Register the API client in PingOne Advanced Identity Cloud using the SSA JWT from the trusted directory for dynamic client registration, (DCR).

    The following example registers an API client through PingGateway. Adapt this to match the realm and redirect URIs you use for testing:

    $ curl \
    --request POST \
    --url 'https://gateway.example.com:8443/am/oauth2/realms/root/realms/alpha/register' \
    --cacert "$HOME/path/to/secrets/gateway.pem" \
    --header 'Content-Type: application/json' \
    --header "ssl-client-cert: ${SSL_CLIENT_CERT}" \
    --data "<client-signed-jwt>"
  5. Save the JSON response.

    It includes the client ID, client secret, and important metadata required for conformance testing.

You have successfully registered the API client using the test trusted directory. Repeat the steps for the other API client unless you have already done so.

When you’ve successfully registered both API clients, you can proceed to the conformance tests.