Skip to content

OpenID4VCI: deferred issuance is unreachable — 202 Accepted is treated as an error, deferred_credential_endpoint is never parsed, and the token/DPoP key are not exposed #1948

Description

@mc1100

Version: observed on 0.99.0; re-checked against main @ 145e9e1a — all three parts unchanged.
Setup: KMP wallet against dev.issuer-backend.eudiw.dev, which ships a *_deferred variant of every credential configuration, so this is reachable in ordinary testing.

What happens

Requesting a deferred configuration returns

202 Accepted
{"transaction_id":"…","interval":60}

and multipaz throws:

IllegalStateException: Error getting a credential issued: 202 Accepted {"transaction_id":"…","interval":60}

There is no way for a wallet to park the pending document and collect it later. Three separate gaps combine to make deferred issuance unimplementable, even outside the library:

1. obtainCredentials treats any non-200 as an error. The 202 never reaches the deferred logic because it dies at the status check:

if (credentialResponse.status != HttpStatusCode.OK) {
Logger.e(TAG,"Credential request error: ${credentialResponse.status} $responseText")
throw IllegalStateException(
"Error getting a credential issued: ${credentialResponse.status} $responseText")

2. deferred_credential_endpoint is never parsed. IssuerConfiguration reads nonce_endpoint and credential_endpoint only, so the endpoint's location is discarded even when the issuer publishes it:

val nonceEndpoint = credentialMetadata.stringOrNull("nonce_endpoint")
val credentialEndpoint = credentialMetadata.string("credential_endpoint")

deferred_credential_endpoint appears nowhere in the repository. The endpoint is not hypothetical — this issuer publishes it in the very metadata document multipaz just parsed:

{
  "credential_issuer": "https://dev.issuer-backend.eudiw.dev",
  "credential_endpoint": "https://dev.issuer-backend.eudiw.dev/wallet/credentialEndpoint",
  "deferred_credential_endpoint": "https://dev.issuer-backend.eudiw.dev/wallet/deferredEndpoint",
  "nonce_endpoint": "https://dev.issuer-backend.eudiw.dev/wallet/nonceEndpoint"
}

3. A caller cannot complete the exchange itself. The access token and the DPoP key are private to the provisioning client — getAuthorizationData() returns opaque CBOR and getDPopKey() is private — so a wallet cannot poll the deferred endpoint outside the library either:

override suspend fun getAuthorizationData(): ByteString? =
if (authorizationData.refreshToken == null) {
null
} else {
ByteString(authorizationData.toCbor())
}

Suggested fix

  1. In obtainCredentials, treat 202 with a transaction_id body as a deferred outcome rather than an error.
  2. Parse deferred_credential_endpoint in IssuerConfiguration.
  3. Expose the outcome — e.g. a ProvisioningModel.CredentialsDeferred(transactionId, interval) state plus a pollDeferred() entry point — so a wallet can store a pending document and collect it later.

Part 1 alone turns a crash into a clear error; all three are needed for the feature.

What we ship instead

Our compatibility layer observes the 202 and its transaction_id (passing the response through unchanged) so the app can tell the user "this issuer provides this document later, which this app cannot collect yet" rather than showing a raw exception. We deliberately do not mark the document as pending, since nothing could ever move it out of that state.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions