# Authentication

> Three ways a mini app authenticates a user — Sollar token exchange, an external identity provider, or none — and the rules none of them bend.

Source: https://miniapp.sollar.com/guides/authentication/

---

The case this platform exists to serve: an employee installs their company's ERP mini app and signs
in with their corporate credentials. That looks simple and is not. It involves three identity
authorities, a prohibition in RFC 8252, and a requirement that two mini apps must not be able to
work out they are serving the same person.

## Choose a mode

| Your situation | Mode |
|---|---|
| Your backend can trust Sollar as the identity provider | `sollar-exchange` |
| Your system has its own identity provider and will not delegate | `external-oauth` |
| No backend identity — a calculator, a reference tool | `none` |

Declare it in the manifest. It cannot be changed at runtime.

## The host is the OAuth client

The Sollar native client is the **confidential client**. Your mini app is not an OAuth client, holds
no `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            │
│  │ your mini app│ ◀────────────────────────┐         │
│  │  (WebView)   │                          │         │
│  └──────────────┘                   ┌──────┴──────┐  │
│                                     │ RFC 8693    │  │
│                                     │ exchange    │  │
│                                     └──────┬──────┘  │
└────────────────────────────────────────────┼─────────┘
                                             │
                                      ┌──────▼──────┐
                                      │  Keycloak   │
                                      └─────────────┘
```

## Mode 1 — `sollar-exchange`

```json title="manifest.json"
"auth": {
  "mode": "sollar-exchange",
  "audience": "acme-erp-backend",
  "scopes": ["erp.read", "erp.approve"]
}
```

In the mini app:

```js
const { access_token } = await sollar.auth.getToken()

const res = await fetch('https://erp.acme.example/api/approvals', {
  headers: { Authorization: `Bearer ${access_token}` }
})
```

That is the whole client side. There is no sign-in screen, because the user is already signed into
Sollar.

Behind it, the native runtime performs:

```http
POST /realms/acme-corp/protocol/openid-connect/token
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<the user's Sollar token>
audience=acme-erp-backend
scope=erp.read erp.approve
```

Keycloak returns a short-lived token whose `aud` is your backend and nothing else. If mini app A
leaks its token, mini app B's backend rejects it on `aud`.

> **NOTE**
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 it does not yet support the `resource`
parameter. Binding the design to `resource` today would not work; when Keycloak adds it, moving is
a precision improvement rather than a rework.

### Verifying on your backend

```js

const jwks = createRemoteJWKSet(
  new URL('https://auth.sollar.com/realms/acme-corp/protocol/openid-connect/certs'))

export async function authenticate(req) {
  const token = req.headers.authorization?.replace(/^Bearer /, '')
  if (!token) throw new Unauthorized()

  const { payload } = await jwtVerify(token, jwks, {
    issuer: 'https://auth.sollar.com/realms/acme-corp',
    audience: 'acme-erp-backend',           // yours, exactly
  })

  return {
    appUserId: payload.sub,                 // the identity you may act on
    scopes: String(payload.scope ?? '').split(' '),
    actingAgent: payload.act?.sub ?? null,  // set when an AI agent is acting
  }
}
```

Check all four: signature, `iss`, `aud`, `exp`. Skipping `aud` is the mistake that makes token
leakage between mini apps exploitable — it is the check that isolates you from every other mini app
on the platform.

## Mode 2 — `external-oauth`

For a system with its own identity authority.

```json title="manifest.json"
"auth": {
  "mode": "external-oauth",
  "issuer": "https://login.acme.example",
  "client_id": "acme-erp-sollar",
  "scopes": ["openid", "erp.read"]
}
```

```js
if (await sollar.auth.state() === 'signed-out') {
  await sollar.auth.signIn()          // opens a system authentication sheet
}
const { access_token } = await sollar.auth.getToken()
```

What happens: the runtime opens **`ASWebAuthenticationSession`** on iOS or **Custom Tabs** on
Android. The user authenticates against your identity provider, seeing a real address bar and a real
lock icon. The redirect returns to the host through a registered scheme; the host exchanges the code
with PKCE and stores the result in the keychain or keystore. Your mini app receives access tokens
and never a refresh token.

> **DANGER**
**This flow never runs in the runtime's WebView, and there is no setting that changes it.**

RFC 8252 §8.12 is explicit that embedded user-agents are forbidden in native-app OAuth flows. Inside
a WebView the user cannot verify the URL or the certificate, and the hosting app is technically
capable of reading the typed password. A third-party mini app asking for corporate credentials
inside a WebView controlled by another third party is precisely the attack the RFC describes.

Customers will occasionally ask for it, "so it feels more integrated". The answer is no.

## Mode 3 — `none`

No identity, no token, no backend session. Declare it honestly; a mini app declaring `none` and then
calling `getToken()` fails validation.

## Rules that do not bend

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

**Every client-side identity claim is a suggestion until your server validates it.** What
`sollar.identity.get()` returns is for drawing the UI. Authorisation happens against the token, on
your server, every time. Telegram publishes the same warning about `tgWebAppData`, and it is the
most common failure in this entire class of platform.

**Short tokens, host-managed renewal.** Call `getToken()` before each use. Do not cache, do not
persist, do not forward.

**Revocation reaches you.** An uninstall, an administrator removing the app, or an employee leaving
revokes the exchange immediately. Because tokens are short, the window is small; because the
exchange runs through Keycloak, the cut is central. Handle `user.deprovisioned` — see
[Server API](/reference/api/server/).

## Agents acting for a user

When a Sollar AI agent invokes one of your actions, the chain records **two** identities: on whose
behalf, and who is acting.

```http
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<the user's token>          ← on whose behalf
actor_token=<the agent's token>           ← who is acting
actor_token_type=urn:ietf:params:oauth:token-type:jwt
audience=acme-erp-backend
```

The resulting token carries an `act` claim:

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

```js
if (auth.actingAgent && amountMinor > tenantPolicy.agentLimitMinor) {
  throw new Forbidden(
    `An agent may approve up to €${tenantPolicy.agentLimitMinor / 100}. ` +
    `This order is €${amountMinor / 100} and needs ${user.name} to approve it directly.`)
}
```

Your backend can distinguish "Ana approved this" from "Ana's agent approved this" — which matters
for audit, for spending limits, and for incident investigation. A tenant may permit an agent to read
but not to approve; the `act` claim is what makes that enforceable.

For delegation across a trust boundary — a Sollar agent acting against the tenant's own identity
provider — the `draft-ietf-oauth-identity-chaining` work is the direction of travel. It is a draft:
treat it as direction, not as a dependency.

## Testing

Your test organisation issues real tokens against a test realm. Permission and configuration changes
take effect immediately, without administrator approval, so the loop is short.

Never point a test organisation at production data. The limit is three test organisations per
developer.
