# Application sign-in and palm approval

Sandbox issuer: `https://sandbox.id.veyns.io`.

Veyns provides a stable account with more than one authentication method. Your application's pairwise `sub` stays the same when that account switches from browser sign-in to a linked palm scanner. The token's `amr` records `veyns:browser` or `veyns:palm`; `auth_time` records the actual ceremony time and never advances on refresh.

## Setup

1. Register the application, exact HTTPS origins and redirect URIs in `/console`.
2. For domain verification, publish DNS TXT `_veyns.<domain>` with `veyns-verification=<verification token>` for **every** registered origin and redirect domain. Editing identity details invalidates verification.
3. Create a backend credential in `/console`. Store it in your backend secret manager. It is shown once; rotation immediately invalidates the old value.
4. For this controlled pilot, staff must admit the application and intended accounts to palm use. Each account connects its Android app and scanner once from `/account`: show the connection QR, scan it in Veyns → Settings → Palm setup → Connect with QR code, then approve the palm-link request. Already connected scanners keep their pairing when the app is updated.

## Sign in

```html
<script src="https://sandbox.id.veyns.io/veyns.js" defer></script>
```

For a working sandbox example, open `/demo-bank/` and inspect `examples/demo-bank/` in the source. It uses a backend-held credential and verifies signatures independently. Its private operator invitation admits one real account; no palm results are simulated.

Call `veyns.signin({clientId: 'your-registered-client'})` from a user gesture. The Veyns screen offers the available methods. Set `method: 'palm'` to require palm. A palm request cannot be completed with the browser method. The popup returns a single-use authorization code, which the SDK exchanges using PKCE and validates against the issuer's JWKS.

For a backend session, generate a single-use nonce on your backend, bind it to the initiating browser with a protected cookie, and pass it as `nonce` to `veyns.signin`. The demo shows this flow.

Servers must independently verify signatures, issuer, audience, expiry, nonce and the required method before establishing their own session. Never trust a browser's decoded claims. Store only the pairwise subject your application received, not a global biometric identifier. Refresh is session continuation and cannot authorize a fresh sensitive action.

The SDK now sends request details to `POST /v1/authorize/prepare` and navigates using an opaque reference. Sensitive actions are no longer accepted in GET `/authorize` URLs. This is a Veyns preparation endpoint, not a claim of RFC 9126 PAR conformance.

## Request approval from your backend

Use HTTP Basic authentication with the registered client id and backend secret. Derive `subject` from your authenticated user session; never accept another user's subject directly from a browser.

`POST /v1/approvals`

```json
{
  "subject": "pairwise:your-registered-client:example-subject",
  "idempotency_key": "your-stable-operation-id",
  "expires_in": 300,
  "action": {
    "statement": "Allow access to the restricted document",
    "details": {"document": "example-document-id"}
  }
}
```

The response contains `request_id`, `challenge`, the canonical action `digest`, status, expiry and an `approval_url`. The same idempotency key with the same input returns the original request; different input returns 409. Expiry is 30–300 seconds. The subject must already be linked to this application through sign-in and have palm connected.

Display the action, open `approval_url` as a popup, or show a waiting state. The link contains a read-only view token in its URL fragment; treat it as private and do not log it. The Android app opens a bottom review panel while Home or Palm setup is idle. With background alerts enabled and the USB scanner connected, it posts a private notification while another app is open. Tapping the notification opens fresh request details; the notification itself cannot approve anything. Only an explicit review and palm verification can approve it. Closing a status popup does not cancel the backend request; call the cancellation endpoint for that.

`GET /v1/approvals/{request_id}` with the same backend credential returns the durable state and, after approval, a signed `decision` JWT. Poll at 2–3 second intervals with a bounded overall deadline. `POST /v1/approvals/{request_id}/cancel` cancels a pending request, including while matching is in progress. Closed requests cannot be approved afterward.

The browser can use:

```js
const result = await veyns.waitForApproval({
  approvalUrl: response.approval_url,
  clientId: 'your-registered-client',
  subject: authenticatedSubject,
  action: originalAction,
  signal: abortController.signal
});
```

This provides a browser notification; your backend must still retrieve and validate the decision.

## Validate, execute, acknowledge

Before execution, validate the signed decision against `/jwks.json` with the application's pinned algorithm and exact issuer/audience. Check `sub`, `request_id`, `request_nonce`, `veyns_intent=action`, `amr` containing `veyns:palm`, `veyns_presence=true`, original action digest, expiry, and a suitable `auth_time`. Do not treat HTTP 200 or a status label alone as authorization.

The action digest is base64url SHA-256 over UTF-8 canonical JSON of `{statement,details}`. Object keys are sorted recursively, arrays retain order, absent details become null, and strings/numbers use JSON serialization. Use the server-provided immutable digest together with the exact original action stored by your backend; do not reconstruct business intent from untrusted browser input.

Atomically record `decision_id`/`request_id` with your own business operation so retries cannot execute it twice. After committing or reconciling that operation, send:

`POST /v1/approvals/{request_id}/ack`

```json
{"decision_id":"decision-id-from-response","operation_id":"your-stable-operation-id"}
```

An identical acknowledgement is safe to retry. A different operation id returns 409. Acknowledgement confirms delivery; it does not prove your system executed the action. Keep polling/reconciliation until you have a terminal decision. Requests and acknowledgements are retained for seven days in this pilot; signed decisions expire after five minutes.

The earlier browser-action flow's `/v1/actions/consume` now requires backend authentication plus `jti`, `action_digest`, and a stable `operation_id`. Anonymous consumption is rejected. Repeating the same acknowledgement succeeds; a conflicting operation returns 409.

## Pilot limits

One Android connection belongs to one Veyns account. Requests cannot be redirected to a different account or scanner by altering the public request body. The app verifies a palm with the Veyns service and returns no global palm identifier to your application. A cancelled scan with an uncertain response resumes its protected delivery record; do not ask the user to scan again until that request has been reconciled.

The current integration trusts the registered Android capture software. It does not provide hardware-signed evidence that a biometric capture was freshly acquired. Device signatures bind transport requests and stop request replay, but cannot prove sensor freshness against compromised terminal software. Do not describe this sandbox as hardware attestation, PAD certification, or production authorization for unrelated customers.

A known browser sends palm sign-in prompts directly to its connected account. An unfamiliar browser shows a sign-in QR once. The server issues a random, HttpOnly, Secure, SameSite=Lax routing cookie after an approved palm sign-in or an authenticated account visit. It lasts up to 30 days and remains after ordinary sign-out; the authentication session still ends. Your account → Browser options → Sign out and forget this scanner explicitly removes the preference. A mismatched account at sign-out also clears it, and routing stops if the account or all scanners are disabled. This is a delivery preference, never an authentication session. Every sign-in requires fresh palm approval. “Use another scanner” cancels the previous pending prompt and returns to QR selection. A QR request is labelled as awaiting a QR scan until a connected scanner claims it.

QR codes are rendered locally, contain an expiring purpose-bound code in a URL fragment, and never go to an external QR service. Connection codes expire after 10 minutes; sign-in requests follow the existing ceremony expiry. Manual code entry remains under More options for camera-less terminals and recovery.

Background alerts use the connected scanner's Android foreground service and a signed inbox poll, normally every 3 seconds with error backoff up to 30 seconds. They require notification permission, an attached scanner, and the app to have been opened; Android power management can delay delivery. A force-stopped app cannot receive them until opened again. The ongoing notification offers Pause alerts. No overlay or automatic background activity is used. FCM/APNs push and outbound webhooks remain separate future adapters. A request that expires while delivery is unavailable still fails closed.
