OpenID4VP / DCQL queries, user profiles and logging (v6.6)

What should the wallet share, and how will your application identify the user afterwards? The DCQL query defines the credentials and claims you request; the profile definition turns the verified result into a user identifier. This page covers both, then shows which logs to enable when a presentation fails.

See also:

  ▸ OpenID4VP overview

  ▸ Clients and configuration

  ▸ Response validation and verifiers


1) The query

Always configure setDcqlQuery(...): it defines what to request and what pac4j will accept. Use a DcqlQuery object or a JSON string; both are checked at initialization.

For EUDI PID, EudiPidQuery.sdJwtVc(...) and EudiPidQuery.mdoc(...) set the format and type for you. Pass the attribute names from EudiPidProfileDefinition, checking that the target wallet supports them. These helpers request the PID, not other credentials such as a separate age attestation.

Optional scope alias: if the wallet supports one, setScope(...) sends that alias instead of the query. You must still configure the equivalent DCQL query for response validation:

DcqlQuery query = EudiPidQuery.sdJwtVc(GIVEN_NAME, AGE_OVER_18);

// Send dcql_query to the wallet and validate the response against it.
OpenId4VpConfiguration dcqlConfig = new OpenId4VpConfiguration()
    .setDcqlQuery(query);

// Send scope to the wallet and validate the response against the equivalent query.
OpenId4VpConfiguration scopeConfig = new OpenId4VpConfiguration()
    .setDcqlQuery(query)
    .setScope("com.example.pid");

com.example.pid is an example alias, not a standard scope. Configuring scope alone is rejected.

JSON alternative:

config.setDcqlQuery("""
    {
      "credentials": [{
        "id": "pid",
        "format": "dc+sd-jwt",
        "meta": {"vct_values": ["urn:eudi:pid:1"]},
        "claims": [
          {"path": ["given_name"]},
          {"path": ["age_over_18"]}
        ]
      }]
    }
    """);

Every returned credential must satisfy the query, including optional credentials that the wallet chooses to send. pac4j enforces requested values on verified claims; wallet-side matching alone is not sufficient. See Response validation.

2) The profile identifier

Request an identifier claim in DCQL if you need to identify a user. A name or an age predicate alone does not provide a persistent user identifier. Choose an identifier that is stable, unique within its issuer and never reassigned (OpenID4VP section 14.4).

The OpenId4VpAuthenticator builds the profile once the presentation is validated, as the other pac4j authenticators do: the disclosed claims become attributes, and the profileIdResolver of the configuration gives the identifier, as attributeAsId does for SAML.

The default, ProfileIdResolver.issuerAndClaim("sub") (for OpenId4VpClient and OpenId4VpDcApiClient):

To use another top-level claim, request it in DCQL and name it:

config.setProfileIdResolver(ProfileIdResolver.issuerAndClaim("account_id"));

account_id is an example; use a claim that meets the identity requirements above.

EUDI PID: EudiWalletClient has no default: the PID defines no sub, and no PID attribute is assumed to be a stable identifier. It refuses to initialize until a resolver is configured, such as ProfileIdResolver.issuerAndClaim(PERSONAL_ADMINISTRATIVE_NUMBER) where the PID provider issues that claim.

Multiple credentials, nested claims (including mdoc) or any other identifier: implement ProfileIdResolver, a single method receiving the verified credentials, indexed by DCQL query identifier:

// an illustrative nested claim: {"account": {"id": "..."}} in the credential answering the "badge" query
config.setProfileIdResolver(credentials -> {
    VerifiedCredential badge = credentials.getVerifiedCredentials().get("badge").get(0);
    return badge.getIssuer() + "|" + ((Map<?, ?>) badge.getClaims().get("account")).get("id");
});

It checks nothing at initialization by default; override its check(DcqlQuery) to verify at startup that the query returns what it needs. To enrich the profile afterwards, set a ProfileCreator on the client: it receives the credentials, whose getUserProfile() is the profile built by the authenticator.

3) Logging

The messages exchanged with the browser, the wallet and the page calling the digital credentials API are logged raw on the PROTOCOL_MESSAGE.OPENID4VP logger, the changes of status of each transaction on the org.pac4j.openid4vp.transaction logger, and the processing outcomes on the org.pac4j.openid4vp loggers, all at DEBUG level:

logging.level.PROTOCOL_MESSAGE.OPENID4VP=DEBUG
logging.level.org.pac4j.openid4vp=DEBUG

The PROTOCOL_MESSAGE.OPENID4VP logger is a child of PROTOCOL_MESSAGE, which enables the messages of all protocols at once. Messages are logged as they travel: an encrypted response stays encrypted, but a response in clear (direct_post or dc_api) holds the personal data the End-User disclosed. Enable these logs to diagnose, not permanently in production. With useRedirectUri=true, the JSON response to the wallet also logs the callback URL and its response_code; treat that code as a temporary secret. The claims of the request objects and the decrypted responses are only logged at TRACE level.