> For the complete documentation index, see [llms.txt](https://developer.vario-software.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.vario-software.de/documentation/apps/authentication.md).

# Authentication

How an App verifies calls from VARIO Cloud and authenticates its own API requests.

An App authenticates in **two directions**, and it is important not to confuse them:

| Direction              | Token                                               | Question it answers                               |
| ---------------------- | --------------------------------------------------- | ------------------------------------------------- |
| VARIO Cloud → your App | **appToken**                                        | Is this incoming request really from VARIO Cloud? |
| Your App → VARIO Cloud | **accessToken** (obtained from an **offlineToken**) | May this App read/write data in this tenant?      |

The **VARIO App Framework** handles both for you. This page explains what it does, so you can debug it — and so you can implement it yourself if you don't use the framework.

***

## The client configuration

Your App is identified by the four values you receive when [creating the App](/documentation/apps/create-an-app.md). The demo app keeps them in `app-client.js`, exporting them as a module:

```javascript
// app-client.js
module.exports = {
  appIdentifier: '…',
  clientId: '…',
  clientSecret: '…',
  appJWK: { keys: [ /* … */ ] },
};
```

You pass this object when constructing the App:

```javascript
const VarioCloudApp = require('@vario-software/vario-app-framework-backend/app.js');
const client = require('../app-client.js');

const app = new VarioCloudApp(client);

app.port = 8443;
app.uiPath = path.resolve(__dirname, '../frontend');

app.offlineToken.init().then(() => app.start());
```

{% hint style="warning" %}
`app.start()` validates the configuration and throws if `appIdentifier`, `clientId`, `clientSecret` or `appJWK` is missing. `appJWK` must be valid JSON — a JSON **string** is parsed automatically, anything that isn't an object is rejected with `appJWK is not a valid JSON`.
{% endhint %}

***

## Inbound: verifying the appToken

Every request VARIO Cloud sends to your App carries an **appToken** — a JWT signed with ES256. The framework installs an authentication middleware on the App's API router, so all your `/api/*` routes are protected automatically.

Verification checks that:

* the signature matches the public key in your `appJWK`,
* the `aud` claim equals your `appIdentifier`,
* the token has not expired.

If any check fails, the request is answered with **401** and never reaches your route.

The token is normally read from the `Authorization: Bearer <appToken>` header. There is one exception: for `GET` requests to `/api/install` and `/api/uninstall`, it is read from the `appToken` **query parameter** — this exists so Apps without a UI can be installed.

{% hint style="info" %}
This is also how you verify **webhooks**: a webhook call into your App is authenticated the same way. If you handle webhooks through the framework's API router, verification already happened. See [Webhooks](/documentation/webhooks/introduction.md).
{% endhint %}

***

## Outbound: from offlineToken to accessToken

To call the VARIO Cloud API, your App needs an access token **per tenant**.

1. During installation, VARIO Cloud hands your App an **offlineToken** for that tenant — long-lived, but not unlimited (see [Lifetime](#lifetime-45-days-sliding)).
2. Your App stores it, keyed by tenant.
3. On the first API call, the framework exchanges the offlineToken for a short-lived **accessToken** at the tenant's identity server.
4. The accessToken is cached until shortly before it expires, then refreshed.

The framework also derives the tenant's **API base URL** from the offlineToken itself: the token's issuer (`iss`) points at the tenant's identity server, and stripping the `sso.` prefix yields the tenant host. Requests are then sent to `https://<tenant-host>/api/vario`.

→ See [REST API Introduction](/documentation/rest-api/introduction.md) for how base URLs are structured.

### Bootstrap: the install call

The offlineToken arrives when VARIO Cloud installs your App in a tenant: it calls your install endpoint with the token in the request body:

```javascript
// api/install
const { offlineToken } = req.body;

if (offlineToken)
{
  await app.offlineToken.set(tenant, offlineToken);
}
```

A typical install service does three things in order:

```javascript
const install = async function ()
{
  await runAsAppUser();      // act as the App's technical user
  await saveOfflineToken();  // persist the tenant's offlineToken
  await migration1();        // create backends, EAV groups, webhooks…
};
```

{% hint style="danger" %}
**Losing the offlineToken means losing API access for that tenant** until it is renewed. Persist it durably before you run anything else, and delete it again on uninstall.
{% endhint %}

### Lifetime: 45 days, sliding

An offlineToken is valid for **45 days** — but the clock restarts on **every use**. Each time your App exchanges it for an access token, its validity is extended to a fresh 45 days.

In practice that means a regularly active App never sees the token expire. It only becomes invalid after **45 consecutive days without a single API call** for that tenant.

{% hint style="warning" %}
This bites Apps that are idle for long stretches — a seasonal integration, a tenant in a test environment, an App that only acts when the customer triggers something.

If that describes your App, make sure it touches the API at least once inside the window (a small scheduled call is enough), or accept that the customer will have to renew the token before the App works again.
{% endhint %}

### Renewal: the install call happens again

An offlineToken is not permanent — it expires if unused, and the customer can replace it deliberately. In the Admin-Center, on the installed App's page, the action **"Offline-Token erneuern"** issues a fresh token and then opens your App's installation page (`pcInstallationUrl`) again with the new token attached — which hands it to your install endpoint exactly as it does on a first install.

In other words: **install is not a one-shot event.** The same endpoint that bootstrapped the App is also the renewal channel.

Two consequences for your implementation:

* **Overwrite unconditionally.** Store the incoming token even when you already have one — otherwise renewal silently keeps the dead token.

  ```javascript
  if (offlineToken)
  {
    await app.offlineToken.set(tenant, offlineToken);   // replaces the previous one
  }
  ```
* **Keep install idempotent.** It will run again on an already-provisioned tenant. Migration steps that already ran are skipped for you, but anything you do outside a migration step must tolerate repetition.

{% hint style="info" %}
When a token has expired, the App instance is flagged accordingly and the customer is prompted to renew it. Your App cannot trigger renewal itself — so when API calls for one tenant start failing with `401`, log the tenant clearly so the cause is recognisable.
{% endhint %}

### Where the offlineToken is stored

Out of the box, `app.offlineToken` keeps tokens in a local JSON file (`offlineToken.db`) — convenient for development, but not suitable for a multi-instance production deployment. You can pass your own storage implementation (`get`, `set`, `delete` per tenant) via the App options and keep tokens in your own database or secrets store.

Changing or deleting a stored offlineToken also drops the cached accessToken for that tenant.

***

## Making API calls

Once installed, you don't manage tokens at all — you use the ERP client on the App:

```javascript
const { data } = await app.erp.vql({
  statement: `SELECT id, articleNumber FROM article.query`,
  limit: 100,
});
```

The framework attaches, on every request:

| Header                    | Value                  |
| ------------------------- | ---------------------- |
| `Authorization`           | `Bearer <accessToken>` |
| `user-agent`              | your `appIdentifier`   |
| `Accept` / `Content-Type` | `application/json`     |

{% hint style="info" %}
The VARIO Cloud API **rejects requests without a `User-Agent`** with `403 Forbidden`. The framework satisfies this by sending your `appIdentifier` — if you call the API without the framework, you must set the header yourself. See [REST API Authentication](/documentation/rest-api/authentication.md).
{% endhint %}

If the API answers `401`, the cached accessToken for that tenant is discarded so the next call fetches a fresh one.

***

## Without the framework

Nothing here is magic — the App model sits on plain OAuth 2.0. If you implement it yourself, you must:

1. Verify the inbound appToken (ES256, `aud` = `appIdentifier`, not expired) against your `appJWK`.
2. Store the offlineToken you receive on install, per tenant.
3. Exchange it for an accessToken using the **refresh token** grant with your `clientId` and `clientSecret`.
4. Send `Authorization: Bearer <accessToken>` **and** a `User-Agent` on every request.
5. Cache access tokens and refresh them before they expire.

The grant types and endpoints are documented in [REST API Authentication](/documentation/rest-api/authentication.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developer.vario-software.de/documentation/apps/authentication.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
