ID Partners ConnectID + the Digital Credentials API

The demo

The connector flow, step by step

A user starts at a relying party, picks a new tile on the chooser, and proves who they are with the licence on their phone. Here is every hop, who signs what, and what each participant can and cannot see.

The pattern is deliberately conservative. To a relying party, the Connector looks like one more provider on the chooser, reached the same way the banks are. To the user's wallet, the Connector is a verifier. Everything new is contained inside one component, and everything difficult about the pattern comes from that seam.

Where ConnectID actually sits

ConnectID is not an exchange in the data path, and this page does not draw it as one. It is the Trust Controller for an OpenID Federation: the anchor that every relying party and every provider chains its identity to. It signs things. It does not route them.

That matters here because it is the mechanism by which the relying party can be named to the wallet. The Connector does not ask the wallet on its own authority; it asks the Trust Controller to sign the request for the relying party that sent the user. Whether the wallet can read that signature is the crux of the whole exercise, and it is taken up below.

This runs

Every hop below is built and deployed. The relying party is a fictional bottle shop with an age-gated checkout, chosen because alcohol delivery is a real Australian use case with a real legal obligation behind it. The walkthroughs show it running, and the demo page has the addresses and what is and is not real.

#The whole flow on one page

sequenceDiagram
  autonumber
  actor U as User (browser)
  participant RP as Relying party
  participant TC as ConnectID<br/>(Trust Controller)
  participant CON as Connector
  participant W as Wallet (on device)

  U->>RP: Wants to prove identity
  RP-->>U: Chooser, rendered by the relying party
  U->>RP: Picks "Digital licence"
  RP->>CON: Redirect. The relying party identifies itself under the ConnectID federation
  CON->>TC: Resolve the relying party's trust chain
  TC-->>CON: A federation member, entitled to ask for these claims
  CON->>TC: Sign the credential request (DCQL) for this relying party
  TC-->>CON: Signed request naming the relying party, anchored in ConnectID
  CON-->>U: Connector page loads
  Note over U,CON: User taps "Use my licence"
  CON->>U: navigator.credentials.get({digital: signed request})
  U->>W: Browser mediates: wallet chooser, then consent
  Note over U,W: The origin is the Connector's.<br/>The signed request names the relying party — if the wallet reads it.
  W-->>CON: Encrypted, signed response, via the browser
  CON->>CON: Verify signatures, check trust list, map to claims
  CON->>RP: Claims returned, as any provider would return them
  RP-->>U: Signed in
ConnectID touches the request and never the response. The Connector wears two hats: to the relying party it is a provider, to the wallet it is a verifier. Two different things identify "who is asking" at step 11, and they do not agree.

#Walking it

#1–3. The user arrives and picks a tile

The chooser is the relying party's screen. It is rendered inside the relying party's page, from a component ConnectID supplies, and the relying party's own server acts on the choice. This is how ConnectID works today, and the only difference is one additional tile sitting alongside the banks.

That tile is a bigger design decision than it looks, and it gets its own treatment in Limitations: placing a licence presentation next to a bank login tells the user the two are interchangeable, and in several respects they are not.

#4. Redirect to the Connector, under the federation

The relying party sends the user to the Connector exactly as it would send them to a bank. What makes this more than a redirect is how the relying party identifies itself: not with credentials it registered with the Connector directly, but with its identity under the ConnectID federation. Its entity statement chains up to ConnectID's trust anchor, and that chain is what the Connector will check.

Why this is the property that makes the pattern attractive

The relying party did not have to register with the Connector, exchange keys with it, or know anything about wallets. It is a federation member, and that is enough. For a relying party already on ConnectID the integration burden is genuinely zero: a new tile appears, and it works. The cost of that convenience is what the rest of this site is about.

#5–6. The Connector asks the Trust Controller who is calling

The Connector resolves the relying party's trust chain through ConnectID. The answer it gets back is not just "this is a member" but "this member is entitled to ask for these claims" — ConnectID's existing per-relying-party attribute certification, carried in federation metadata rather than in a contract someone has to look up.

This is the step that keeps ConnectID's rules in force. A relying party certified for an age check cannot use the Connector to ask for a full licence, because the trust chain says what it may ask for and the Connector enforces it.

#7–8. The Trust Controller signs the request for the relying party

The Connector builds the credential request — the query that says which fields are wanted, in the query language the wallet protocol defines — and asks the Trust Controller to sign it on behalf of the relying party.

This is the load-bearing step. Read it twice.

The request the wallet receives is signed under ConnectID's trust anchor and names the relying party as the verifier. Not the Connector. So a wallet that understands the federation can look at the request and see, in cryptographic terms, "Wattlebrook Cellars is asking, and ConnectID vouches for it". The Connector is the party running the ceremony; it is not the party the request says is asking.

Two constraints attach. The signed request only exists on the protocol Android uses; the iPhone path has no slot for it, as the next step explains. And the wallet is not obliged to read it — the specification leaves that to the wallet's discretion. So the mechanism exists and the demo shows it working; whether it works in the field depends on what wallets choose to render.

#9–10. The Connector page, and the tap

The Connector serves a page. The page waits for the user to tap, because the API requires a real user gesture, and then makes the call, carrying the signed request.

It has to be a real top-level browser page. Not an embedded frame without explicit permission, not a native app's in-app browser. The redirect model means nobody in this flow controls the browsing context the user arrives in; the user often does not control it either, because they followed a link from an email client or a social app that opens pages in its own embedded browser. This is treated properly under the in-app browser problem.

#11. The browser mediates, the wallet asks

The browser takes over. It finds the wallets, shows the chooser if there is more than one, and hands off to the chosen wallet. The wallet shows the user what is being requested and by whom, and asks for consent, usually with a biometric.

"By whom" is where the two hats collide. The browser tells the wallet the origin that made the call, and that is the Connector's. The signed request says the relying party is asking, under ConnectID's authority. Those are two different answers to the same question, and which one the user sees depends on the platform and the wallet:

  • On Android, a wallet that reads the signed request can show "Wattlebrook Cellars, via ConnectID". A wallet that does not shows the Connector's origin and is conformant in doing so.
  • On iPhone, there is no signed request. The protocol carries the verifier's identity as a certificate chain with no second-party field, and the wallet shows the origin. The relying party is invisible, and nothing in the current protocol changes that.

This is the finding the whole site turns on, and it is worked through in the wallet thinks it is talking to the Connector and can the relying party be named to the wallet.

The Connector's page sees none of the wallet interaction. It cannot tell how many wallets there were, which one was chosen, how long the user took, or whether the user cancelled or had no licence at all. See errors are opaque by design.

#12. The response comes back

The wallet's response returns through the browser to the Connector's page, encrypted to the Connector's key, and the page posts it to the Connector's backend.

The response is signed twice, as described in how the API works. The device signature covers a transcript that includes the origin that asked, and that is the Connector's. This binding is what makes the response unforgeable and un-replayable. It is also what makes it, cryptographically, a presentation to the Connector — whatever the signed request said about who was asking.

#13. The Connector verifies

The backend checks the issuer's signature against a trust list of issuing authorities, checks the device signature against the transcript, checks the validity window, and maps the licence data elements onto the claim names the relying party is certified to receive.

This is where the Connector earns its keep, and where it accumulates responsibility: holding the reader credentials, maintaining the trust list, and deciding what a valid presentation means. It is also the only point in the flow at which any party other than the user sees the licence data in the clear.

#14–15. Back to the relying party

The Connector returns the claims to the relying party the way any provider would. From here the flow is completely conventional. ConnectID was never in the path of the response and never sees it.

#What each participant can see

This table is the fastest way to see where the pattern's tensions sit.

Participant Sees the user's identity Knows which relying party Knows what was asked for
Relying party Yes, the released claims Yes, it asked
ConnectID (Trust Controller) No. It signs the request and never sees the response Yes, it signed for them Yes, it signed the query
Connector Yes, in full Yes, it resolved their trust chain Yes
Wallet / issuing authority It is the user's own device Android: if it reads the signed request. iPhone: no Yes

Three rows deserve a second look.

The ConnectID row is better than the naive version of this pattern would give you, and it is worth being precise about why. ConnectID learns that a particular relying party asked for particular fields — the same transaction metadata it holds today — and nothing else. It is not in the data path. That property is preserved because the Trust Controller signs the request rather than proxying the response.

The Connector row is the one to take to a risk meeting. The Connector sees the complete licence response, in the clear, for every user who takes this path. It has to: it is the party that decrypts and verifies it. Whether it then keeps anything is a design choice that must be made explicitly, and the identifier problem pushes hard towards keeping something. Whether the Connector is operated by ConnectID, or by someone else under ConnectID's federation, is therefore not a detail — it decides whether "ConnectID never sees customer data" stays true.

The wallet row is the one to take to a policy meeting. On one platform the relying party can be named, if the wallet cooperates. On the other it cannot be named at all. A road agency being asked to authorise readers of its licences will reasonably ask which of those it is signing up to, and the honest answer today is "both, depending on the phone".