Skip to content

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.

Your situationMode
Your backend can trust Sollar as the identity providersollar-exchange
Your system has its own identity provider and will not delegateexternal-oauth
No backend identity — a calculator, a reference toolnone

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

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 │
└─────────────┘
manifest.json
"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/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.

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.

For a system with its own identity authority.

manifest.json
"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.

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

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.

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-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:

{
"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.

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.