DEV Community

DarkEdges
DarkEdges

Posted on

Custom ID-JAG on PingFederate, Part 2: Building the Generator and Handling Runtime Constraints

The signing code was only one part of getting a custom ID-JAG generator working on PingFederate 12.3.3.1.

The more revealing work was discovering what the token-exchange pipeline validates before calling the generator, and what it actually passes into the plugin.

This article describes the implementation that passed the live local proof. It issues an assertion for one fixed destination. It does not configure downstream redemption.

Code for this series: project repository.

Start with a small, explicit contract

The final generator receives six mapped attributes:

  • sub: the validated subject carried through the exchange policy.
  • subject_client_id: the validated subject token's audience.
  • subject_exp: the validated subject token's expiry in Unix seconds.
  • approved_scope: scopes authorized by the trusted policy.
  • authenticated_client_id: PingFederate's authenticated client context.
  • http_request: PingFederate's actual servlet request context object.

There is an important separation here. The HTTP request supplies the requested destination and scope, but it does not establish who the client is. Client identity comes from the server's authenticated context.

We also found that this 12.3.3 token-generator exchange path creates a TokenContext without populating its input parameters. Relying on getInParameters() for authentication or routing information would have been incorrect. The implementation uses explicitly mapped subject attributes instead.

Read request parameters without enabling expressions

The first configuration attempted expression-based mappings for audience, resource and scope. The running server rejected those mappings.

Instead of enabling expressions globally, the final mapping passes Context HttpRequest directly to the plugin. The generator extracts the underlying HttpServletRequest object and validates its parameters in Java.

The essential helper is:

private static String parameter(HttpServletRequest request, String name) {
    String[] values = request.getParameterValues(name);
    if (values == null || values.length != 1) {
        throw new IllegalArgumentException(
                "Missing or repeated request parameter: " + name);
    }
    return nonempty(values[0]);
}
Enter fullscreen mode Exit fullscreen mode

nonempty() also rejects blank values and control characters. This is an excerpt from the plugin, not a standalone implementation.

Using getParameterValues() matters. Selecting only the first value could hide duplicate parameters. The live suite confirmed rejection of repeated audience, resource and scope parameters, including repeated identical values.

This is a deliberately strict request profile. Do not assume that accepting multiple resources or audiences can be added without revisiting the policy.

Bind the caller to the subject token

The generator compares both authenticated_client_id and subject_client_id with the configured requesting client.

The JWT processor validates the subject token's audience. The policy additionally requires azp to match the requesting client and sub to match the authorized test subject.

Our proof requires azp even when an intended real issuer might omit it. That is a restriction of this implementation, not a claim that every ID token must contain the claim. Real issuer integration needs an explicit review of those semantics.

Treat approved scope as authorization data

The requested scopes must be contained in both the generator's configured scope ceiling and the policy-provided approved_scope set.

Copying the incoming scope directly into approved_scope would defeat that check.

In this lab, the approved scope is a fixed value behind issuance criteria for one authorized subject and client. Production deployment needs a real entitlement decision for the user, client and destination.

Satisfy the audience check before the generator

The live server validates each token-exchange audience as a local OAuth client ID. An unknown value fails before the custom generator can run.

Our outgoing assertion needs the target authorization-server issuer as its audience. To preserve that value, we created a local compatibility registration whose client ID is exactly:

https://target.example.test
Enter fullscreen mode Exit fullscreen mode

This is not the requesting client and not the client registration at the downstream server.

The compatibility client remains disabled. It has an independent unused secret and only the ACCESS_TOKEN_VALIDATION grant, because the Terraform provider requires a nonempty grant list.

Two live checks established the boundary: issuance succeeded with that registration disabled, and an authentication attempt using it was rejected. This is observed behavior of the tested server, not a blanket compatibility promise for every PF version.

Route by resource and sign with managed keys

The generator group maps the exact resource URI to the ID-JAG requested token type. PingFederate required one default mapping inside the group. We configured that mapping without making the group a global default.

The plugin gets the current RS256 key through the public SDK accessor:

JwksEndpointKeyAccessor.newInstance().getCurrentRsaKey("RS256")
Enter fullscreen mode Exit fullscreen mode

It signs with typ set to oauth-id-jag+jwt, includes the key's kid, and creates a fresh jti. Expiry is the earlier of the configured lifetime and the validated subject-token expiry.

The compact JWT is returned as a StringSecurityToken. The live server preserved that string without an additional Base64 wrapper, and the signature verified against its published JWKS.

Generating a unique jti is not replay prevention. The receiver still needs to enforce replay handling when the assertion is redeemed.

Keep the remaining boundary visible

This implementation validated the issuing side with a locally trusted subject fixture. Real issuer login, downstream acceptance, production entitlements and multi-node behavior remain separate work.

Part 3 makes the working configuration repeatable with Terraform, including the resources that the pinned Ping provider does not expose directly.

Top comments (0)