Intercept and debug OID4VP/VCI traffic between a wallet and a verifier/issuer. Point the wallet at the proxy instead of the real server. Every request and response is captured, classified by protocol step, decoded, and shown in the terminal and a live web dashboard.
eudi proxy --target http://localhost:8080
eudi proxy --target http://localhost:8080 --port 9090 --dashboard 9091
eudi proxy --target http://localhost:8080 --no-dashboardWallet <--> Proxy (:9090) <--> Verifier/Issuer (:8080)
|
Live dashboard (:9091)
The proxy can launch the target service as a subprocess and scan its output for encryption keys:
eudi proxy --target http://localhost:3000 -- mvn spring-boot:run
eudi proxy --target http://localhost:3000 -- npm startThe proxy checks the rules from top to bottom. The first match sets the badge.
| Badge | Detected when |
|---|---|
| VCI metadata | Path contains .well-known/openid-credential-issuer |
| OIDC metadata | Path contains .well-known/openid-configuration |
| VCI credential offer | credential_offer or credential_offer_uri in query |
| VP auth request | client_id and a response_type with vp_token in query. An id_token response type counts with an OID4VP parameter such as request_uri, response_uri or dcql_query |
| OIDC auth request | client_id, redirect_uri and a response_type with code, token or id_token in query, plus the openid scope or a form_post response mode |
| OIDC callback | GET with code or error in query, or a POST body with code or error and no grant_type |
| VP request object | GET whose response body is a JWT, or a POST body with wallet_metadata or wallet_nonce (request_uri_method=post) |
| VCI nonce request | POST to a path ending in /nonce whose response contains c_nonce |
| VP auth response | POST body contains vp_token, presentation_submission, id_token or response (JARM) |
| OIDC token request | POST to /protocol/openid-connect/token, a response with id_token, or an authorization_code grant with redirect_uri |
| VCI token request | POST to a path ending in /token |
| VCI credential request | POST to a path ending in /credential or /credentials |
When no rule matches, a request to an endpoint advertised in earlier traffic gets that endpoint's badge. Metadata, authorization requests and request objects advertise endpoints.
By default only OID4VP/VCI traffic is shown. Other requests (favicon, health checks) are proxied but hidden. --all-traffic shows everything.
- Decoding: payloads are decoded inline (SD-JWT, JWT, mdoc, DCQL queries, JWE headers)
- Credential decode hints: detected credentials are printed as
eudi decodecommands - JARM/JWE decryption: a
direct_post.jwtresponse from the built-in wallet is decrypted - Flow correlation: related protocol steps are grouped by shared
state/noncevalues - Web dashboard at
http://localhost:9091with live SSE updates and HAR export - JARM/JWE detection: shows encrypted response headers and the verifier's ephemeral public key
- NDJSON output:
--jsonprints one JSON object per line
The proxy forwards every body in full. It keeps the first MiB of each request and response body for the terminal, the dashboard and the HAR export. A longer body is marked requestTruncated or responseTruncated in --json. When the proxy keeps the whole response, it replaces the target host with its own. A longer response passes through unchanged.
Event streams pass through as each event arrives. Event streams and long polls stay open past the server's write timeout, because each write gets a new deadline.
| Flag | Default | Description |
|---|---|---|
--target |
None | URL of the verifier/issuer (required) |
--port |
9090 |
Proxy listen port |
--dashboard |
9091 |
Dashboard listen port |
--listen |
$EUDI_DEV_LISTEN, else 127.0.0.1 |
IP address the proxy and its dashboard bind. 0.0.0.0 opens them to the network (see listen address) |
--no-dashboard |
false |
Disable web dashboard |
--dashboard-base-url |
None | Public dashboard URL when a reverse proxy serves it under a path prefix, such as https://example.com/eudi-proxy (see reverse proxy setup) |
--all-traffic |
false |
Show all traffic |
--json |
false |
NDJSON output to stdout (global flag) |
-- <command> |
None | Launch target as subprocess, scan stdout |
eudi proxy logs [dashboard-url] reads the traffic of a running proxy:
| Flag | Default | Description |
|---|---|---|
[dashboard-url] |
http://localhost:9091 |
Dashboard of the proxy to read |
-f, --follow |
false |
Keep printing as the proxy records traffic |
--json |
false |
Print the recorded traffic as JSON (global flag) |
Use eudi proxy logs to read traffic from a proxy running in a container, in the background or on another machine:
# a proxy on this machine
eudi proxy logs
# a proxy whose dashboard port is published from a container
eudi proxy logs http://localhost:9091 --follow
# somewhere else entirely
eudi proxy logs https://proxy.internal.example --followThe output matches the proxy's terminal output, with decode links to its dashboard. --follow streams new traffic and reconnects after a disconnect or restart. On reconnect, it reads any missed entries still in the proxy's history. --json prints the recorded traffic once and cannot be combined with --follow.
A proxy started without --all-traffic records only OID4VP/VCI requests, so logs shows only those.
The dashboard serves the recorded traffic as JSON. eudi proxy logs reads these routes.
| Route | Response |
|---|---|
GET /api/entries |
All recorded entries as a JSON array. The X-Entries-Limit header gives how many entries the proxy keeps |
GET /api/stream |
New entries as server-sent events, one JSON entry per data: line |
GET /api/har |
All recorded entries as a HAR file, downloaded as eudi-dev.har. The dashboard's HAR export uses it |
curl -o traffic.har http://localhost:9091/api/har━━━ [14:32:05] GET /authorize?client_id=... ← 200 (45ms) [VP auth request]
┌ client_id: did:web:verifier.example
┌ response_mode: direct_post.jwt
┌ nonce: abc123
┌ dcql_query: { "credentials": [...] }
━━━ [14:32:05] GET /request/abc123 ← 200 (12ms) [VP request object]
┌ header: {"alg":"ES256","typ":"oauth-authz-req+jwt"}
┌ payload: { ... }
━━━ [14:32:06] POST /response ← 200 (89ms) [VP auth response]
┌ response_type: JWE (decrypted via debug key)
┌ encryption_alg: ECDH-ES
┌ response_payload: {"vp_token":{...},"state":"abc123"}
→ eudi decode 'eyJhbGci...' (vp_token)
When the built-in wallet (eudi wallet) sends an encrypted JARM response (direct_post.jwt) through the proxy, the proxy decrypts the payload and shows the contained vp_token and state.
The wallet puts the AES content encryption key (CEK) in the X-Debug-JWE-CEK header. The proxy strips the header before forwarding, so the verifier never sees it. Route the wallet through the proxy:
eudi wallet serve # wallet sends to response_uri
eudi proxy --target http://verifier # proxy intercepts, decrypts, forwards
A third-party wallet sends no debug header. When the verifier service runs as a subprocess (with --), the proxy scans its stdout for CEK values and uses them to decrypt JWE responses:
eudi proxy --target http://localhost:3000 -- mvn spring-boot:runThe proxy detects lines like:
CEK: <base64url>orcontent encryption key: <base64url>- JWK objects containing a
"d"(private key) parameter
Without a key the proxy shows the JWE header fields only (alg, enc, kid, epk).
- The wallet logs the content encryption key of each encrypted response to stdout:
[VP] JWE content encryption key for proxy debugging: <base64url CEK> - Output of a service launched with
--appears with a[service]prefix. - Pipe
--jsonoutput tojq:eudi proxy --target ... --json | jq '.credentials'