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:
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
- In
obtainCredentials, treat 202 with a transaction_id body as a deferred outcome rather than an error.
- Parse
deferred_credential_endpoint in IssuerConfiguration.
- 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.
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*_deferredvariant of every credential configuration, so this is reachable in ordinary testing.What happens
Requesting a deferred configuration returns
and multipaz throws:
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.
obtainCredentialstreats any non-200 as an error. The202never reaches the deferred logic because it dies at the status check:multipaz/multipaz/src/commonMain/kotlin/org/multipaz/provisioning/openid4vci/OpenID4VCIProvisioningClient.kt
Lines 219 to 222 in 145e9e1
2.
deferred_credential_endpointis never parsed.IssuerConfigurationreadsnonce_endpointandcredential_endpointonly, so the endpoint's location is discarded even when the issuer publishes it:multipaz/multipaz/src/commonMain/kotlin/org/multipaz/provisioning/openid4vci/IssuerConfiguration.kt
Lines 66 to 67 in 145e9e1
deferred_credential_endpointappears 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 andgetDPopKey()is private — so a wallet cannot poll the deferred endpoint outside the library either:multipaz/multipaz/src/commonMain/kotlin/org/multipaz/provisioning/openid4vci/OpenID4VCIProvisioningClient.kt
Lines 127 to 132 in 145e9e1
Suggested fix
obtainCredentials, treat202with atransaction_idbody as a deferred outcome rather than an error.deferred_credential_endpointinIssuerConfiguration.ProvisioningModel.CredentialsDeferred(transactionId, interval)state plus apollDeferred()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
202and itstransaction_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.