# Identity

> Four layers of identifier, why app_user_id cannot be correlated, and how the host acts as the OAuth client.

Source: https://miniapp.sollar.com/platform/identity/

---

An identity system for a mini app store has to answer a question that a single application never
faces: **can two mini apps work out that they are serving the same person?**

If the answer is yes, the store is a tracking network. Two apps compare identifiers and reconstruct
a person's behaviour across unrelated services, and no privacy policy fixes that, because it is a
property of the architecture.

## Four layers

The model follows Lark's, which handles this most carefully.

| Layer | Scope | Exposed to |
|---|---|---|
| `sollar_id` | Global, immutable, unique in Sollar | **Nobody.** It never leaves Sollar infrastructure. |
| `user_id` | Unique within a tenant | Tenant administrators, administrative APIs |
| `app_user_id` | Unique per (user, mini app) | The mini app and its backend |
| `vendor_id` | Unique per (user, developer) | Optional, with explicit consent |

### `app_user_id` is the load-bearing one

```
app_user_id = HMAC(tenant_key, sollar_id ‖ mini_app_id)
```

Two mini apps serving the same human receive different, derived, non-correlatable identifiers. The
tenant key lives in an HSM or KMS.

Three consequences worth stating:

- A breach of one mini app's database does not expose the underlying Sollar identity.
- Two mini apps cannot join their data on a shared key, even if both want to.
- A tenant can **rotate its key** and break every correlation at once — a real incident-response
  capability, not a theoretical one.

### `vendor_id` is the deliberate exception

A company running several mini apps has a legitimate reason to recognise the same user across them —
the case Lark and DingTalk serve with `unionid`. Sollar supports it, with two conditions: the mini
apps must share a `vendor.id`, and the user must consent explicitly. It is not the default, and it
is not silent.

## The host is the OAuth client

The Sollar native client is the **confidential client**. A mini app is never an OAuth client, never
holds a `client_secret`, and never sees a refresh token.

This is the Backend-For-Frontend pattern that RFC 10017 (BCP 212, *OAuth 2.0 for Browser-Based
Applications*) §6.1 recommends for browser applications — and a mini app is, technically, a browser
application.

```
┌───────────────────────────────────────────────────────┐
│ Sollar native client   (confidential client)          │
│                                                       │
│  ┌──────────────┐   audience-scoped token             │
│  │ mini app     │ ◀─────────────────────────┐         │
│  │ (WebView)    │                           │         │
│  └──────────────┘                    ┌──────┴───────┐ │
│                                      │ RFC 8693     │ │
│                                      │ exchange     │ │
│                                      └──────┬───────┘ │
└─────────────────────────────────────────────┼─────────┘
                                              │
                                       ┌──────▼──────┐
                                       │  Keycloak   │
                                       └─────────────┘
```

The exchange is keyed on `audience`, not on RFC 8707's `resource`: Keycloak's Standard Token Exchange
V2 is GA and is the internal-to-internal path, but does not yet support `resource`. When it does,
migrating improves precision; it does not block anything today.

**A token for one mini app is useless to another.** The restricted `aud` guarantees it: if mini app A
leaks its token, mini app B's backend rejects it.

## Three paths

| Path | For | Mechanism |
|---|---|---|
| `sollar-exchange` | A backend that trusts Sollar as IdP | RFC 8693 exchange in the native runtime |
| `external-oauth` | A system with its own IdP | `ASWebAuthenticationSession` / Custom Tabs, PKCE |
| `none` | No backend identity | — |

> **DANGER**
`external-oauth` never runs in the runtime's WebView. RFC 8252 §8.12 forbids embedded user-agents in
native-app OAuth flows: the user cannot verify the URL or certificate, and the host is technically
able to read the typed password. A third-party mini app requesting corporate credentials inside a
WebView controlled by another third party is exactly the attack the RFC describes.

There is no configuration that changes this, and customers do ask.

## Agent delegation

When a Sollar agent acts, the chain records **two** identities — on whose behalf, and who is acting.
RFC 8693 with `actor_token` produces a token carrying an `act` claim:

```json
{ "sub": "u_9f3a…", "act": { "sub": "agent_admin_assistant" }, "aud": "acme-erp-backend" }
```

A mini app's backend can therefore distinguish "Ana approved this" from "Ana's agent approved this".
That matters for audit, for authority limits — a tenant may let an agent read but not approve — and
for incident investigation.

For delegation across a trust boundary, `draft-ietf-oauth-identity-chaining` (v17) is the
standards-track direction. It is a draft: a direction, not a dependency.

## Rules

**No long-lived secret reaches the runtime.** No `client_secret`, no API key, no refresh token, no
signing key. WeChat documents the equivalent about its own `session_key` — *"the developer server
should not send the session key to the Mini Program"* — for the same reason.

**Client-side identity is a suggestion until the server validates it.** What
`sollar.identity.get()` returns is UI convenience. Authorisation happens against the token, on the
backend, every time. Telegram publishes the same warning about `tgWebAppData`, and it remains the
most common failure in this platform class.

**Short tokens, host-managed renewal.** The mini app calls `getToken()` before each use; it does not
cache, persist or forward.

**Revocation is central.** An uninstall, an administrator removing the app, or an employee leaving
revokes the exchange immediately. Short tokens keep the window small; the exchange running through
Keycloak makes the cut central.

## Server callback authentication

When a mini app's backend calls Sollar, the request is signed — Slack's recipe, the best-documented
of its kind:

```
base       = "v0:" + timestamp + ":" + raw_body
signature  = "v0=" + hex(HMAC_SHA256(signing_secret, base))
headers    X-Sollar-Signature, X-Sollar-Request-Timestamp
```

Constant-time comparison, and a short timestamp window to block replay. HMAC-SHA256, not SHA-1 —
Lark's `jsapi_ticket` scheme uses SHA-1 and works, but SHA-1 is not a defensible choice for a design
started in 2026.

## Open questions

- **RFC 8707 `resource` in Keycloak.** When it lands, migrating from `audience` improves precision.
- **Keycloak Organizations** is GA since 26.0.0 and is the intended B2B multi-tenancy base, but has
  not been validated under load on Sollar's topology.
- **Cross-domain identity chaining** depends on an unfinished IETF draft.
- **Keycloak inside mainland China** under PIPL and the national firewall remains unconfirmed — an
  inherited infrastructure risk that applies here unchanged.
