DEV Community

DarkEdges
DarkEdges

Posted on

Custom ID-JAG on PingFederate, Part 3: Managing the Integration with Terraform

Once the custom ID-JAG generator worked, the next requirement was to make its PingFederate configuration repeatable.

The goal was not just to automate a sequence of API calls. We wanted Terraform to own identifiable resources, refresh their state and produce a reviewable plan.

The result manages eight resources on the local server reporting PingFederate 12.3.3.1. After apply, a follow-up plan returned no changes.

Code for this series: project repository.

Separate plugin deployment from configuration

Terraform configures the installed plugin. It does not deploy the JAR in this project.

The Java build produces xaa-id-jag-generator.jar. That file must be placed in the server's server/default/deploy directory, followed by a restart and verification that the Admin API discovers its descriptor.

The live test used a copy into the named local container. The repository also includes an optional Compose overlay for a read-only JAR mount. That overlay was syntax-checked, but it was not the deployment mechanism used for the successful live run.

Keeping these steps separate makes the dependency visible: an API resource cannot configure a plugin that the server has not loaded.

Check the provider schema, not only its examples

We pinned these provider versions:

terraform {
  required_version = ">= 1.10, < 2.0"
  required_providers {
    pingfederate = {
      source  = "pingidentity/pingfederate"
      version = "1.9.0"
    }
    restapi = {
      source  = "Mastercard/restapi"
      version = "3.0.0"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Inspection of the installed Ping provider schema confirmed that version 1.9.0 does not implement token-generator instances or token-exchange generator groups, despite references to those resources in some examples.

The provider does implement the JWT processor, exchange policy, OAuth clients and generator mapping we needed. We used it for those resources.

For the missing resources, we used the REST provider against the actual Admin API. Its object resource supports resource lifecycle operations and refresh, rather than just running a command during apply.

This split is specific to the pinned versions. Recheck the schema before assuming the same gap exists in a future release.

The eight resources

The complete configuration manages:

  1. A JWT Token Processor 2.0 instance for the trusted subject issuer.
  2. A Token Exchange Processor Policy with subject and azp issuance criteria.
  3. A confidential requesting client restricted to token exchange and the approved scope.
  4. A disabled compatibility client whose ID is the downstream issuer URL.
  5. The custom ID-JAG generator instance.
  6. A resource-selected generator group.
  7. The policy-to-generator attribute mapping.
  8. One common scope entry.

The REST provider owns the generator, generator group and individual scope entry. Everything else uses the Ping provider.

Managing the scope individually avoids taking ownership of the entire authorization-server settings object. We did not change global expression settings, global key configuration or the global default generator group.

Model the real API resources

The generator-group excerpt looks like this:

resource "restapi_object" "generator_group" {
  path                    = "/oauth/tokenExchange/generator/groups"
  ignore_server_additions = true

  data = jsonencode({
    id           = "xaaGroup"
    name         = "XAA explicit resource routing"
    resourceUris = [var.target_resource]
    generatorMappings = [{
      requestedTokenType = "urn:ietf:params:oauth:token-type:id-jag"
      tokenGenerator     = { id = restapi_object.generator.id }
      defaultMapping     = true
    }]
  })
}
Enter fullscreen mode Exit fullscreen mode

The group depends on the generator's resource ID. Its mapping is the default within this group, which the live API required. The group itself is not a global default.

ignore_server_additions accommodates server-added fields. It is not ignore_all_server_changes; the configuration should still own the fields it explicitly sets. The observed post-apply result was a clean plan, not a comprehensive test of every possible drift scenario.

Keep the administrator password out of state

The administrator password is a sensitive, ephemeral input:

variable "admin_password" {
  type      = string
  sensitive = true
  ephemeral = true
}
Enter fullscreen mode Exit fullscreen mode

The provider uses it for authentication. A check of the resulting state confirmed that the supplied administrator password was absent.

OAuth client secrets are different. They are resource inputs and are stored in state. Marking them sensitive hides their normal display; it does not encrypt the state file.

The local fixture files and state are ignored by Git. Shared use still needs an encrypted, access-controlled backend, appropriate secret injection and protected local files. A Git ignore rule is not a security boundary.

TLS verification defaults to enabled. The disposable localhost fixture explicitly opts into a localhost-only certificate exception. Remote configurations must use a trusted certificate or a configured CA file.

Plan before applying

From the repository root, with provider credentials supplied through the documented environment variables:

terraform '-chdir=terraform' init -input=false
terraform '-chdir=terraform' validate
terraform '-chdir=terraform' plan '-var-file=../.local/local.tfvars.json' '-out=../.local/xaa.tfplan'
Enter fullscreen mode Exit fullscreen mode

Inspect the plan before applying the saved file. On our fresh test server, the initial plan contained eight additions and no existing-resource updates or deletions.

The first apply exposed server validation requirements described in Part 2. Terraform retained the successfully created resources in state, and the corrected plan completed the remaining configuration. We did not discard state and start creating duplicate objects.

After the successful apply, plan -detailed-exitcode returned zero with no changes. Four mocked Terraform tests also passed, checking the restricted configuration and rejecting remote TLS bypass, private JWKS input and shared client secrets.

Those tests do not prove token issuance. Part 4 covers the separate live runner, the evidence it produced and the work that remains before this could become a production integration.

Top comments (0)