Web wallets

In this section

This page describes how a check-in page, the Verifier, and a web wallet exchange a SMART Health Check-in request and response when the wallet is a website instead of an app on the phone.

Only the transport differs from a native wallet. The Verifier opens the wallet in a tab, and the two pages talk with postMessage. The SMART request and response, the mdoc wrapping, and the encryption are exactly what the Digital Credentials API carries (spec §8).

The library does this for you

You need the rest of this page only to implement the exchange yourself, or to debug it.

Side Use
Verifier wallets({ registry: "/wallets.json" }), or webWallet(entry) for one wallet, then wallet.start(request) inside the click
Verifier, no code <smart-checkin-picker registry="/wallets.json"> from /ui
Web wallet serveWebWallet({ onRequest }) from /wallet; see the Wallet guide

Verifiers find web wallets in a wallet registry.

Sequence

  1. The Verifier opens the wallet in a new tab.
  2. The wallet posts ready.
  3. The Verifier posts the request.
  4. The patient reviews and chooses in the wallet.
  5. The wallet posts the response and may close itself.
  6. The Verifier decrypts and checks the response, as it would a native wallet's.

Opening the wallet

Browsers let a page open a tab only while it handles the patient's click, so the Verifier calls window.open(walletUrl) in the click handler, before any await. walletUrl comes from the wallet's registry entry, and the wallet opens in a new tab unless the entry has "target": "popup".

Building the request can take long enough for the browser to stop treating the page as handling a click, so open the tab first and build the request after. The wallet may then post ready before the request exists; send the request once both have happened.

Keep the window reference that window.open returns. The Verifier accepts a message only when its event.source is that window, its event.origin is the wallet's origin, and, for a response, its requestId matches the request.

The three messages

Ready: wallet to Verifier

The wallet sends this as soon as it loads.

window.opener.postMessage({ type: "digital-credentials/web-wallet/ready" }, "*");

It carries no data, so "*" is safe here.

Request: Verifier to wallet

The Verifier sends this after ready, to the wallet's origin only, never to "*".

walletWindow.postMessage({
  type: "digital-credentials/web-wallet/request",
  requestId: "<opaque, unique per request>",
  credentialRequestOptions: {
    digital: { requests: [
      { protocol: "org-iso-mdoc", data: { deviceRequest, encryptionInfo } }
    ] }
  }
}, walletOrigin);

credentialRequestOptions is the same argument the Verifier would pass to navigator.credentials.get (VRQ-8).

Response: wallet to Verifier

The wallet sends this once, to the Verifier's origin only, with the request's requestId. It has one of three outcomes:

// The patient shared
{ type: "digital-credentials/web-wallet/response", requestId, outcome: "approved",
  credential: { protocol: "org-iso-mdoc", data: { response } } }

// The patient cancelled
{ type: "digital-credentials/web-wallet/response", requestId, outcome: "declined" }

// Something failed
{ type: "digital-credentials/web-wallet/response", requestId, outcome: "error", message: "<what went wrong>" }

response is the base64url dcapiResponse, exactly what a native wallet returns (HPKE-2). A wallet should send declined or error rather than closing without a reply, so the Verifier can tell the patient what happened at once.

The Verifier's origin

The wallet learns who is asking from the browser, never from the message.

Processing in the wallet

A web wallet processes the request the same way a native wallet does (§8.4):

Timeouts and closing

The Verifier treats the wallet's window closing before a response as a decline. It also gives up after a timeout, which in this library is five minutes. A wallet can't extend the timeout, even for a long form.