You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While integrating ssi-agent into a real Laravel application with public-internet wallets (Sphereon Wallet, UniMe), I hit two server-side OID4VCI interop gaps. Either of them is enough on its own to break a wallet flow; together they break it twice.
I currently work around both by putting a Laravel/Caddy translation shim in front of the agent. With the shim, Sphereon Wallet completes credential issuance end-to-end. Without the shim, it doesn't reach the credential endpoint.
Filing here on the agent because both are server-side: the metadata response and the credential-request deserializer.
The response does not include nonce_endpoint, authorization_servers, or token_endpoint, even though the agent already routes:
POST /openid4vci/nonce
GET /.well-known/oauth-authorization-server (which does carry the token endpoint)
Why it matters
Per OpenID4VCI 1.0:
nonce_endpoint is REQUIRED in issuer metadata when the wallet is expected to retrieve a server-generated nonce before calling the credential endpoint.
authorization_servers is RECOMMENDED so wallets know which authorization-server-metadata document to fetch.
Concrete observed behaviour:
Wallet
Behaviour against agent without the shim
Sphereon 804 (iOS)
Falls back to GET /.well-known/oauth-authorization-server, recovers the token endpoint that way, eventually succeeds. Lenient parser.
UniMe 0.13.2 (iOS)
Stops after fetching metadata. No further HTTP requests at all. (Cross-filed: see the companion issue on impierce/identity-wallet.)
So even Impierce's own wallet does not work against this agent without the shim — strong signal that the metadata is currently incomplete.
Suggested fix
In agent_api_http::v0::issuance::credential_issuer::well_known::openid_credential_issuer, after returning the stored credential_issuer_metadata, populate:
nonce_endpoint from public_url + /openid4vci/nonce
authorization_servers: [issuer_url]
(optionally) token_endpoint directly inline for older clients that don't follow the OAuth-metadata indirection
The values are already known to the agent at runtime — they just aren't serialised into the well-known response. Injecting these via reverse proxy was sufficient to make Sphereon's flow work end-to-end.
…deserialisation produces proofs = None. NonceValidationService::extract_nonce_from_credential_request returns an empty Vec, validate() returns MissingNonce, and credential.rs maps that to:
HTTP/1.1 400 Bad Request
{ "error": "invalid_nonce" }
The same request with proofs: { "jwt": ["<same jwt>"] } succeeds — proving the nonce is fine. The error is misleading: the wallet developer sees invalid_nonce and starts debugging nonce flow, when the actual problem is field naming.
Reproduction
After getting an access_token and c_nonce:
# Fails with invalid_nonce
curl -X POST https://your-agent/openid4vci/credential \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"credential_configuration_id":"my_custom_credential", "proof":{"proof_type":"jwt","jwt":"<jwt with c_nonce>"}}'# Same JWT, different wrapping shape: succeeds
curl -X POST https://your-agent/openid4vci/credential \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"credential_configuration_id":"my_custom_credential", "proofs":{"jwt":["<same jwt>"]}}'
UniMe 0.13.2 — never reaches this step in our test, but presumably affected too
Suggested fix (any of)
Bump the oid4vci crate to a revision that supports both shapes via #[serde(alias)] or a custom deserializer.
Add a one-line alias in the agent's wrapper struct so proof deserialises into proofs.jwt[0].
At the very least, return a more descriptive error: invalid_request with a body like {"error":"invalid_request","error_description":"unsupported credential request shape; expected 'proofs.jwt'"}. The current invalid_nonce is a real footgun for integrators — I lost a couple of hours to it before reading the source.
Related
build: bump OID4VCI dependencies #283 (closed, released on @beta) — the OID4VCI dep bump from draft 15 → 1.0 that introduced this shape change. Backward compatibility for wallets still on the older shape was apparently not in scope.
Workaround in place
For anyone hitting these in the meantime, here is the shim approach that gets Sphereon working:
Front the agent with Caddy at an HTTPS hostname (mandatory for any real wallet).
Path-route GET /.well-known/openid-credential-issuer and POST /openid4vci/credential to a small Laravel shim that:
Injects nonce_endpoint, authorization_servers, and token_endpoint into the issuer metadata response.
Rewrites incoming proof (singular) request bodies to proofs (plural array) before forwarding to the agent.
Everything else proxies straight to the agent.
Happy to share the shim source if useful.
Are you planning to contribute this in a PR?
I can take a stab at Issue 1 (extending the well-known metadata serialiser) if it'd land cleanly. Issue 2 is more invasive (touches a pinned external crate); I'd defer to maintainers on the right approach.
Materials I can provide on request
Full agent log of a working Sphereon attempt (with shim) and a failing UniMe attempt.
The exact issuer metadata responses (raw + post-shim) and the my_custom_credential configuration.
Description
While integrating
ssi-agentinto a real Laravel application with public-internet wallets (Sphereon Wallet, UniMe), I hit two server-side OID4VCI interop gaps. Either of them is enough on its own to break a wallet flow; together they break it twice.I currently work around both by putting a Laravel/Caddy translation shim in front of the agent. With the shim, Sphereon Wallet completes credential issuance end-to-end. Without the shim, it doesn't reach the credential endpoint.
Filing here on the agent because both are server-side: the metadata response and the credential-request deserializer.
Environment
impiercetechnologies/ssi-agent:1.0.0-beta.17(org.opencontainers.image.revision=2e7763efe50be245fffd9e142127927d0251a55d)UNICORE__PROFILE=developmenthttps://ssi-agent.example.com(Caddy + Let's Encrypt)A custom
my_custom_credentialcredential configuration is registered at runtime viaPOST /v0/credential-configurations(format: jwt_vc_json).Issue 1 — Issuer metadata is missing fields current wallets expect
Reproduction
The response does not include
nonce_endpoint,authorization_servers, ortoken_endpoint, even though the agent already routes:POST /openid4vci/nonceGET /.well-known/oauth-authorization-server(which does carry the token endpoint)Why it matters
Per OpenID4VCI 1.0:
nonce_endpointis REQUIRED in issuer metadata when the wallet is expected to retrieve a server-generated nonce before calling the credential endpoint.authorization_serversis RECOMMENDED so wallets know which authorization-server-metadata document to fetch.Concrete observed behaviour:
GET /.well-known/oauth-authorization-server, recovers the token endpoint that way, eventually succeeds. Lenient parser.impierce/identity-wallet.)So even Impierce's own wallet does not work against this agent without the shim — strong signal that the metadata is currently incomplete.
Suggested fix
In
agent_api_http::v0::issuance::credential_issuer::well_known::openid_credential_issuer, after returning the storedcredential_issuer_metadata, populate:nonce_endpointfrompublic_url+/openid4vci/nonceauthorization_servers: [issuer_url]token_endpointdirectly inline for older clients that don't follow the OAuth-metadata indirectionThe values are already known to the agent at runtime — they just aren't serialised into the well-known response. Injecting these via reverse proxy was sufficient to make Sphereon's flow work end-to-end.
Related but distinct
/.well-known/openid-credential-issuerfor custom entries — asks for user-customisable fields (DID methods, signing algs). This issue is about missing required fields, not user-extensibility.Issue 2 — Credential endpoint rejects the older
proof(singular) shape with misleadinginvalid_nonceThe
oid4vcidependency pinned inCargo.toml(rev = "2f27e41", brought in by #283 which bumped to draft 1.0) only deserialises the newer plural shape:There is no
#[serde(alias = "proof")]and no fallback parser. So when a wallet sends the older shape:{ "credential_configuration_id": "my_custom_credential", "proof": { "proof_type": "jwt", "jwt": "<jwt-with-correct-nonce>" } }…deserialisation produces
proofs = None.NonceValidationService::extract_nonce_from_credential_requestreturns an emptyVec,validate()returnsMissingNonce, andcredential.rsmaps that to:The same request with
proofs: { "jwt": ["<same jwt>"] }succeeds — proving the nonce is fine. The error is misleading: the wallet developer seesinvalid_nonceand starts debugging nonce flow, when the actual problem is field naming.Reproduction
After getting an
access_tokenandc_nonce:Affected wallets observed
proof(singular)Suggested fix (any of)
oid4vcicrate to a revision that supports both shapes via#[serde(alias)]or a custom deserializer.proofdeserialises intoproofs.jwt[0].invalid_requestwith a body like{"error":"invalid_request","error_description":"unsupported credential request shape; expected 'proofs.jwt'"}. The currentinvalid_nonceis a real footgun for integrators — I lost a couple of hours to it before reading the source.Related
Workaround in place
For anyone hitting these in the meantime, here is the shim approach that gets Sphereon working:
GET /.well-known/openid-credential-issuerandPOST /openid4vci/credentialto a small Laravel shim that:nonce_endpoint,authorization_servers, andtoken_endpointinto the issuer metadata response.proof(singular) request bodies toproofs(plural array) before forwarding to the agent.Happy to share the shim source if useful.
Are you planning to contribute this in a PR?
I can take a stab at Issue 1 (extending the well-known metadata serialiser) if it'd land cleanly. Issue 2 is more invasive (touches a pinned external crate); I'd defer to maintainers on the right approach.
Materials I can provide on request
my_custom_credentialconfiguration.