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) Diagnostic logging

DEBUG logs follow transaction creation, request construction and capability negotiation, wallet response reception, transaction consumption, decryption, credential verification, DCQL checks and profile creation. Transaction identifiers link these stages; credential query identifiers identify the presentations being checked. Rejections report the failing stage and, for common protocol checks, the reason. Custom verifier failures are logged by exception type without copying their potentially sensitive messages. Wallet error responses are logged at WARN on receipt, including their error code and optional description, without waiting for the browser callback.

For example, enable these categories in Logback:

<logger name="org.pac4j.openid4vp.config" level="DEBUG"/>
<logger name="org.pac4j.openid4vp.redirect" level="DEBUG"/>
<logger name="org.pac4j.openid4vp.request" level="DEBUG"/>
<logger name="org.pac4j.openid4vp.credentials" level="DEBUG"/>
<logger name="org.pac4j.openid4vp.dcql" level="DEBUG"/>
<logger name="org.pac4j.openid4vp.verifier" level="DEBUG"/>
<logger name="org.pac4j.openid4vp.profile" level="DEBUG"/>

These logs describe stages and outcomes without dumping wallet URLs, request JWTs, presentations, keys, nonces, state values or disclosed claim values. Client and profile-definition classes also inherit DEBUG logs from pac4j-core which can include credentials, profiles or converted attribute values. Enabling DEBUG for the entire module or framework also enables those inherited logs; the categories above allow tracing the protocol without enabling them.