Cover image for Understanding Optional SSAs in FDX: DCR with and without Software Statements

Understanding Optional SSAs in FDX: DCR with and without Software Statements

In the world of Dynamic Client Registration (DCR), one acronym tends to trip up even experienced implementers: SSA. While Open Banking specs like the UK's and Brazil's mandate Software Statement Assertions, the FDX spec takes a different approach—SSAs are optional. But what does that mean in practice, and how do you build for both worlds without shooting yourself in the foot?

What's a Software Statement Assertion (SSA)?

An SSA is a signed JWT issued by a trusted authority—typically a central directory or governance body—that vouches for a client's identity and metadata during the registration process. Think of it as a tamper-proof introduction letter between your fintech app and a data holder's authorisation server (AS).

This assertion contains claims like:

  • Client name and description
  • Redirect URIs
  • Grant types, response types
  • JWKS URI for key discovery
  • Organisation and software identifiers
  • Signature and expiry time
Without it, an AS has no basis to trust what the client is claiming.

In heavily governed ecosystems like the UK's Open Banking or Open Finance Brasil, the SSA is non-negotiable. No SSA, no onboarding. But FDX throws a curveball: it's not mandatory.

FDX's Position: SSA Optionality

The FDX specification allows for both SSA-based and SSA-less registration. This flexibility makes sense when you consider the North American ecosystem:

  • No central directory comparable to the UK's OB Directory
  • Wider range of bilateral agreements between TPPs and data holders
  • Desire to support legacy OAuth setups where SSA infrastructure doesn't exist

However, this optionality means additional complexity for implementers. As a fintech, you can't just assume all banks will follow the same rules. Some will expect a software_statement. Others will ignore it entirely.

When to Include the SSA—and When Not To

From an implementation standpoint, here are typical conditions for each path:

  • Include an SSA if the data holder:
    • Publishes SSA requirements in their developer portal
    • Is part of a closed ecosystem that provides SSA issuance
    • Rejects registrations missing software_statement
  • Skip the SSA if:
    • The data holder explicitly says it's not required
    • You're using mutual TLS or manual vetting as a trust anchor
    • The registration process is tightly coupled to a commercial agreement

    Ideally, this decision should be made dynamically per target AS, based on metadata or environment configuration.

Annotated SSA Payload (JWT Claims)

{
  "client_name": "My Fintech App",
  "redirect_uris": ["https://myapp.com/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "jwks_uri": "https://myapp.com/jwks.json",
  "scope": "openid accounts",
  "software_id": "abc123-456def",
  "software_roles": ["data_recipient"],
  "org_id": "fintech-co",
  "authority_url": "https://fdx.directory.example",
  "iss": "https://fdx.directory.example",
  "exp": 1715208397,
  "iat": 1715204797
}

Key fields:

  • software_id: Unique to your software product, issued by the directory
  • org_id: Your legal entity identifier, registered with the governance body
  • jwks_uri: Where your public keys live for token and request signing
  • authority_url: The directory or SSA authority that issued this JWT

Sequence Diagram: Supporting Both SSA and Non-SSA Flows

Web Sequence Diagram of DCR with or without an SSA
View Sequence Diagram Code
Client -> ConfigStore: Load AS config
alt SSA required
  Client -> Directory: Request SSA
  Directory --> Client: Signed JWT (SSA)
  Client -> AS: POST /register (with SSA)
  AS -> SSA: Verify signature, claims
  SSA --> AS: Validated
  AS --> Client: Client ID issued
else SSA not required
  Client -> AS: POST /register (no SSA)
  AS --> Client: Client ID issued (trust via mTLS / internal policy)
end

Node.js Example: Registering with and without SSA

Here's a basic Node.js implementation using Axios that supports both paths:

const axios = require("axios");

async function getSSA(directoryUrl) {
  const { data } = await axios.get(directoryUrl);
  return data; // Assumes the response is the signed JWT
}

async function registerClient({ metadata, config }) {
  const payload = { ...metadata };

  if (config.requireSSA) {
    payload.software_statement = await getSSA(config.directoryUrl);
  }

  const response = await axios.post(config.registerUrl, payload, {
    headers: { "Content-Type": "application/json" },
    httpsAgent: config.mtlsAgent
  });

  return response.data;
}

This snippet uses a `requireSSA` flag and gracefully degrades if the SSA isn't needed. You could extend this to support fetching a fresh JWKS before registration or caching SSAs locally.

Pro Tips from the Trenches

  • Validate the SSA before sending it—check expiry, issuer, and audience
  • Cache SSA tokens per client for reuse across environments
  • Don't hardcode JWKS URIs—load them dynamically via SSA or config
  • Log rejected SSA claims clearly—they're your best debug signal
  • If the AS returns invalid_software_statement, verify base64 encoding and JWT structure

Closing Thoughts

The optional SSA model in FDX is a classic case of “flexibility at a cost.” It opens the door to lightweight integrations where appropriate, but it also demands that implementers be smarter, more adaptable, and slightly paranoid.

If you're building a unified DCR client across multiple ecosystems, treat SSA as a plug-in: load it when required, skip it when not, and validate it always. And remember—just because it's optional doesn't mean it's never needed.