# Countersign

> Have the signed-in person confirm one exact action before your app carries it out, and receive a signed proof bound to that action alone.

Source: https://docs.trustaige.com/countersign

Replace these placeholders with your organization's own values:

- `<your-client-id>`: App client ID, for example `q3Jx8vN2mZ0bT5sLwK7yHc4dR1fGaE9uPiO6nVjXkMs`. On the app’s page in the hub, under Credentials.
- `<your-app-url>`: Your app’s address, for example `https://app.example.com`. Scheme and host, no path: where your app runs.
- `<your-client-secret>`: Client secret, a secret. From the hub: on the app’s page under Credentials. Keep it on your server.

A passkey proves who signed in. It doesn't stop malware already on a signed-in
laptop from using that live session in your app. Countersign asks the person
to confirm one sensitive action (a change of salary account, a role grant, a
deletion, a refund) at the moment it happens. They see exactly what they are
agreeing to, and your server receives a signed proof bound to that one action,
which it checks before it acts.

Your organization's admin decides, per app, how the person countersigns:

| Setting | What the person does |
|---|---|
| **Passkey** | A Trustaige window opens over your page. They read the action and confirm with their passkey. |
| **Phone** | They approve in the Trustaige Identity app on their phone. Not yet available to organizations. |

Your app makes the same request either way and never chooses the method.

1. Person → Your page: saves a change
2. Your page → Your server: the change
3. Your server → Trustaige: POST /countersign/bc-authorize
4. Trustaige → Your server: auth_req_id, frame_url, txn_hash
5. Your page → Trustaige: countersign.js opens frame_url
6. Person ↔ Trustaige: reads the action, passkey
7. Your server → Trustaige: POST /countersign/token
8. Trustaige → Your server: verification_proof
9. Your server → Person: checks the proof, applies the change

## Quickstart

1. **Turn it on for your app.** In the hub, open **Apps**, choose your app and
   open its **Countersign** tab. Pick **Passkey**. Countersign is for
   applications your organization runs itself, signing people in with OpenID
   Connect and authenticating as themselves (a client secret or a key), so the
   tab appears only for those. Then press **Try Countersign** to see the whole
   experience on yourself; nothing changes in your app.

2. **Ask Trustaige when a sensitive change comes in.** Describe the action
   (see [Describe the action](/countersign/describe)) and post it to
   `bc-authorize` from your server, authenticated as your app, exactly as at
   the token endpoint. Store `auth_req_id` and `txn_hash` with the pending
   change.

   ```js title="server.js"
   const details = [{
     type: 'acme.payroll.bank_account.change',
     summary: `Change where ${employee.name} is paid`,
     fields: [
       { name: 'account_number', title: 'New account', value: `•••• ${account.slice(-4)}`, format: 'emphasis' },
       { name: 'bank', title: 'Bank', value: bank, format: 'bold' },
     ],
   }];
   const res = await fetch('https://id.trustaige.com/countersign/bc-authorize', {
     method: 'POST',
     headers: {
       Authorization: 'Basic ' + Buffer.from(`<your-client-id>:${process.env.CLIENT_SECRET}`).toString('base64'),
       'Content-Type': 'application/x-www-form-urlencoded',
     },
     body: new URLSearchParams({
       login_hint: session.sub,
       authorization_details: JSON.stringify(details),
       frame_origin: '<your-app-url>',
     }),
   });
   const started = await res.json();
   if (!res.ok) throw new Error(started.error_description || started.error);
   await pending.save({ id: change.id, authReqId: started.auth_req_id, txnHash: started.txn_hash, details, sub: session.sub });
   return { method: started.method, frameUrl: started.frame_url, code: started.binding_code };
   ```

   ```go title="countersign.go"
   form := url.Values{
       "login_hint":            {session.Sub},
       "authorization_details": {string(details)},
       "frame_origin":          {"<your-app-url>"},
   }
   req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
       "https://id.trustaige.com/countersign/bc-authorize", strings.NewReader(form.Encode()))
   req.SetBasicAuth("<your-client-id>", os.Getenv("CLIENT_SECRET"))
   req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   resp, err := http.DefaultClient.Do(req)
   if err != nil {
       return err
   }
   defer resp.Body.Close()
   var started struct {
       AuthReqID   string `json:"auth_req_id"`
       Method      string `json:"method"`
       FrameURL    string `json:"frame_url"`
       BindingCode string `json:"binding_code"`
       TxnHash     string `json:"txn_hash"`
       Error       string `json:"error"`
   }
   if err := json.NewDecoder(resp.Body).Decode(&started); err != nil || resp.StatusCode != http.StatusOK {
       return fmt.Errorf("countersign refused: %s", started.Error)
   }
   return pending.Save(ctx, change.ID, started.AuthReqID, started.TxnHash, details, session.Sub)
   ```

   ```python title="countersign.py"
   started = requests.post(
       "https://id.trustaige.com/countersign/bc-authorize",
       auth=("<your-client-id>", os.environ["CLIENT_SECRET"]),
       data={
           "login_hint": session["sub"],
           "authorization_details": json.dumps(details),
           "frame_origin": "<your-app-url>",
       },
       timeout=10,
   )
   body = started.json()
   if started.status_code != 200:
       raise RuntimeError(body.get("error_description", body["error"]))
   pending.save(change.id, auth_req_id=body["auth_req_id"], txn_hash=body["txn_hash"],
                details=details, sub=session["sub"])
   ```

   ```bash
   curl https://id.trustaige.com/countersign/bc-authorize \
     -u '<your-client-id>:<your-client-secret>' \
     --data-urlencode 'login_hint=<sub>' \
     --data-urlencode 'authorization_details=[{"type":"acme.payroll.bank_account.change","summary":"Change where Kemi Adeyemi is paid","fields":[{"name":"account_number","title":"New account","value":"•••• 4321","format":"emphasis"}]}]' \
     --data-urlencode 'frame_origin=<your-app-url>'
   ```

3. **Open the Trustaige window on your page.** Load the script once and hand
   it the `frame_url`. It covers your page with the pass and resolves when the
   person has answered. Disable your own submit button until then, so the
   change can't be sent twice.

   ```html title="employee.html" {1,6}
   <script src="https://id.trustaige.com/countersign.js" defer></script>
   <script>
     async function save(change) {
       saveButton.disabled = true;
       const started = await post('/changes', change);
       const outcome = await TrustaigeCountersign.open(started.frameUrl);
       // approved, declined, ended or unavailable: your server decides
       await post(`/changes/${started.id}/collect`);
       saveButton.disabled = false;
     }
   </script>
   ```

   The outcome only tells your page to look. Never apply a change because the
   page says `approved`: the proof travels server to server.

4. **Collect the proof and check it before you act.** Poll the token endpoint
   with the `auth_req_id`, no faster than `interval` seconds. When the answer
   is a proof, run every check in [Check the proof](/countersign/proof), then
   apply the change.

   ```js title="server.js"
   const res = await fetch('https://id.trustaige.com/countersign/token', {
     method: 'POST',
     headers: {
       Authorization: 'Basic ' + Buffer.from(`<your-client-id>:${process.env.CLIENT_SECRET}`).toString('base64'),
       'Content-Type': 'application/x-www-form-urlencoded',
     },
     body: new URLSearchParams({ auth_req_id: change.authReqId }),
   });
   const answer = await res.json();
   if (answer.error === 'authorization_pending' || answer.error === 'slow_down') return 'waiting';
   if (!res.ok) return refuse(change, answer.error);
   await checkProof(answer.verification_proof, change);
   await apply(change);
   ```

   ```bash
   curl https://id.trustaige.com/countersign/token \
     -u '<your-client-id>:<your-client-secret>' \
     -d 'auth_req_id=<auth_req_id>'
   ```

## The request

`POST https://id.trustaige.com/countersign/bc-authorize`, form-encoded, with
your app's client authentication as it is registered: `client_secret_basic`,
`client_secret_post` or `private_key_jwt`. A `private_key_jwt` assertion is
addressed (`aud`) to `https://id.trustaige.com` or its token endpoint,
exactly as for the token endpoint. A public client can't ask.

| Parameter | |
|---|---|
| `login_hint` | Required. The `sub` your app received for the person when they signed in. |
| `authorization_details` | Required. The action, as a JSON array: see [Describe the action](/countersign/describe). |
| `binding_message` | Optional, up to 140 characters. The headline when no entry has a `summary`. |
| `frame_origin` | Required for Passkey. The origin of the page that opens the window, which must be the origin of one of your app's redirect URIs. |
| `nonce` | Optional. Echoed in the proof. |

A `200` answer:

| Member | |
|---|---|
| `auth_req_id` | The request. Collect its proof with it, and check it against the proof's `jti`. |
| `method` | `same_device_passkey` or `phone_approval`, as the admin set it. |
| `frame_url` | Passkey only. Hand it to `TrustaigeCountersign.open`. |
| `binding_code` | Phone only. Show it beside your own "Approve on your phone"; the person picks the same code on their phone. |
| `txn_hash` | The value the proof's `txn_hash` will carry. Store it with the pending change. |
| `expires_in`, `interval` | Seconds until the request ends (5 minutes for Passkey, 3 for Phone), and between polls (2). |

An app set to Phone gets no `frame_url`: there is no Trustaige window, and
your page shows its own wait with the `binding_code`. Handle both methods, so
your app keeps working when the admin changes the setting.

## The answer

`POST https://id.trustaige.com/countersign/token` with `auth_req_id` and the
same client authentication. A `200` answer is
`{"verification_proof": "<JWT>", "token_type": "verification+jwt"}`. A proof
is collected once, and is valid for two minutes from the person's answer, so
keep polling while the request is open.

| `error` | Means | Do |
|---|---|---|
| `authorization_pending` | The person hasn't answered yet. | Poll again after `interval`. |
| `slow_down` | You polled faster than `interval`. | Wait longer between polls. |
| `access_denied` | The person declined, or reported it as not theirs. | Don't apply the change. |
| `expired_token` | Nobody answered before the request ended. | Ask again if the person still wants it. |
| `invalid_grant` | The proof was already collected, or the request is another app's. | Don't apply the change. |

## Troubleshooting

**“unauthorized_client”**

Countersign is off for the app, or the app isn't one your organization runs
itself: an app made from a catalog template can't use it. An admin turns it
on in the app's **Countersign** tab.

**“invalid_request”: frame_origin**

`frame_origin` must be the scheme and host (and port) of one of the app's
redirect URIs, such as `<your-app-url>`. Add the page's origin as a redirect
URI, or send the one you already have.

**“unknown_user_id”**

`login_hint` must be the `sub` from the person's ID token for this app, and
the person must still have access to the organization.

**“invalid_authorization_details”**

The action isn't in the shape Countersign shows. The `error_description` says
which entry and field, for example that a field needs a `title`. **Try it** in
the hub shows the same reason for a request you paste in.

**“phone_approval_unavailable”**

The app is set to Phone, and the person hasn't set up Trustaige Identity for
approvals, or Phone isn't available to the organization. Trustaige never
switches to the passkey instead: tell the person, or ask the admin to set the
app to Passkey.

**The window opens, but the passkey prompt doesn't**

Your page sets a `Permissions-Policy` that blocks
`publickey-credentials-get` for frames. Allow it for
`https://id.trustaige.com`.

## Next

- [**Describe the action**](/countersign/describe): the fields the person
  reads, and how long an action should be.
- [**Check the proof**](/countersign/proof): every check your server makes
  before it acts.
