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
Section titled “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
Section titled “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
Section titled “Mode 1 — sollar-exchange”"auth": { "mode": "sollar-exchange", "audience": "acme-erp-backend", "scopes": ["erp.read", "erp.approve"]}In the mini app:
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:
POST /realms/acme-corp/protocol/openid-connect/tokengrant_type=urn:ietf:params:oauth:grant-type:token-exchangesubject_token=<the user's Sollar token>audience=acme-erp-backendscope=erp.read erp.approveKeycloak 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.
Verifying on your backend
Section titled “Verifying on your backend”import { createRemoteJWKSet, jwtVerify } from 'jose'
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
Section titled “Mode 2 — external-oauth”For a system with its own identity authority.
"auth": { "mode": "external-oauth", "issuer": "https://login.acme.example", "client_id": "acme-erp-sollar", "scopes": ["openid", "erp.read"]}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.
Mode 3 — none
Section titled “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
Section titled “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.
Agents acting for a user
Section titled “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.
grant_type=urn:ietf:params:oauth:grant-type:token-exchangesubject_token=<the user's token> ← on whose behalfactor_token=<the agent's token> ← who is actingactor_token_type=urn:ietf:params:oauth:token-type:jwtaudience=acme-erp-backendThe resulting token carries an act claim:
{ "sub": "u_9f3a…", "act": { "sub": "agent_admin_assistant" }, "aud": "acme-erp-backend"}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
Section titled “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.