Flavoso APIGrundlagenAuthentifizierung

Authentifizierung

OAuth 2.0 mit PKCE: wie ein Inhaber deine App mit seinem Betrieb verbindet und wie du Tokens tauschst, erneuerst und widerrufst.

Überblick

Zugriff auf einen Betrieb bekommt eine App nur mit der Zustimmung seines Inhabers. Flavoso nutzt dafür OAuth 2.0 mit Autorisierungscode (RFC 6749) und PKCE mit S256 (RFC 7636), dazu den Parameter iss bei der Rückkehr (RFC 9207), Widerruf nach RFC 7009 und Metadaten nach RFC 8414.

  1. 1

    Deine App

    „Mit Flavoso verbinden“

    Deine App erzeugt state und code_verifier und schickt den Browser an /oauth/authorize.

  2. 2

    Flavoso

    Anmelden

    Der Inhaber meldet sich mit seinem Flavoso-Konto an, mit zweitem Faktor, wenn er einen hat.

  3. 3

    Flavoso

    Zustimmen

    Er wählt den Betrieb, liest Name, Website und Rechte deiner App und drückt „Verbinden“.

  4. 4

    Browser

    Zurück mit code

    Flavoso leitet mit code, state und iss auf deine redirect_uri zurück. Der code gilt 10 Minuten und nur einmal.

  5. 5

    Dein Server

    Tauschen

    Dein Server tauscht code und code_verifier gegen Access- und Refresh-Token.

  6. 6

    Dein Server

    Anfragen

    Mit dem Access-Token (1 Stunde) an die API; das Refresh-Token holt neue.

EndpunktAdresse
Autorisierung (Browser)https://app.flavoso.com/oauth/authorize
TokenPOST https://api.flavoso.com/oauth/token
WiderrufPOST https://api.flavoso.com/oauth/revoke
Metadatenhttps://api.flavoso.com/.well-known/oauth-authorization-server

Die Autorisierung ist eine Seite auf app.flavoso.com, denn dort meldet sich der Inhaber an. Token, Widerruf und Metadaten liegen auf https://api.flavoso.com. Die Endpunkte mit allen Parametern stehen auch in der Referenz.

Vertrauliche und öffentliche Clients

Den Client-Typ wählst du beim Anlegen der App:

  • Vertraulich (confidential): für Apps mit eigenem Server. Sie bekommen ein Client-Secret (flv_cs_…), das genau einmal angezeigt wird und nur auf den Server gehört. Am Token-Endpunkt melden sie sich per HTTP Basic an oder schicken client_id und client_secret im Rumpf. Echte Betriebe verbinden sie nur über eine https-Redirect-URI.
  • Öffentlich (public): für Apps, die kein Geheimnis sicher verwahren können, etwa eine Kassen-App auf einem Tablet oder ein Programm auf dem Rechner im Laden. Sie bekommen kein Secret und schicken nur client_id. Sie dürfen Loopback-Adressen und ein eigenes Schema benutzen (RFC 8252), auch für echte Betriebe.

PKCE mit S256 ist für beide Pflicht. Der Token-Endpunkt nimmt keine Aufrufe aus dem Browser an (kein CORS); eine Web-App tauscht Code und Refresh-Token deshalb auf ihrem Server und ist damit ein vertraulicher Client.

Die Client-ID (flv_app_…) ist nicht geheim. „Neues Client-Secret erzeugen“ in der Konsole ersetzt das Client-Secret; das alte gilt danach noch 24 Stunden, so stellst du ohne Unterbrechung um.

Redirect-URIs

Nach der Zustimmung schickt Flavoso den Browser an eine der Redirect-URIs deiner App zurück. Für sie gilt:

  • Flavoso vergleicht Zeichen für Zeichen mit den eingetragenen Adressen. Einzige Ausnahme ist der Port bei den Loopback-Adressen.
  • https, ohne Anmeldedaten in der Adresse. Darüber verbinden echte Betriebe jede App.
  • Loopback (http://127.0.0.1:<port>/…, http://localhost:<port>/…, http://[::1]:<port>/…, beliebiger Port, RFC 8252) und eigene Schemata mit Punkt (com.example.pos:/callback): bei einer öffentlichen App für jeden Betrieb, bei einer vertraulichen App nur für deinen eigenen Test-Betrieb. Der Zustimmungsbildschirm bietet dann nur ihn an.
  • Kein Fragment (#), höchstens 300 Zeichen, höchstens 10 Adressen je App.

Autorisierung anfragen

Deine App schickt den Browser an https://app.flavoso.com/oauth/authorize, mit diesen Parametern:

ParameterPflichtBedeutung
response_typejaImmer code.
client_idjaDie Client-ID deiner App.
redirect_urijaEine der eingetragenen Adressen, genau so geschrieben.
scopeneinDie gewünschten Rechte, durch Leerzeichen getrennt, nur aus den Rechten deiner App. Ohne scope fragst du alle Rechte deiner App an.
statejaEin zufälliger Wert (bis 500 Zeichen), den du dir merkst und nach der Rückkehr vergleichst. Schützt vor untergeschobenen Antworten.
code_challengejaBASE64URL(SHA-256(code_verifier)) ohne = am Ende. Den code_verifier (43 bis 128 Zeichen) behältst du bis zum Tausch.
code_challenge_methodjaImmer S256.
import { createHash, randomBytes } from "node:crypto";

// code_verifier: 43 to 128 characters of A-Z a-z 0-9 - . _ ~ (32 random bytes give 43)
const codeVerifier = randomBytes(32).toString("base64url");
// code_challenge = BASE64URL(SHA-256(code_verifier)), without "=" padding
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const state = randomBytes(16).toString("base64url");
// Keep codeVerifier and state (e.g. in the session) until the redirect comes back.

const url = new URL("https://app.flavoso.com/oauth/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "flv_app_...",
  redirect_uri: "https://example.com/flavoso/callback",
  scope: "orders:write menu:write store:write",
  state,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
});
// Redirect the browser to url.href

Die Adresse leitet auf die Sprache des Browsers weiter. Anfrage und Rückkehr sehen so aus:

# Authorization request (one line, wrapped here for reading)
https://app.flavoso.com/oauth/authorize?response_type=code
  &client_id=flv_app_...
  &redirect_uri=https%3A%2F%2Fexample.com%2Fflavoso%2Fcallback
  &scope=orders%3Awrite%20menu%3Awrite%20store%3Awrite
  &state=STATE
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256

# Connected: back at your redirect_uri
https://example.com/flavoso/callback?code=AUTHORIZATION_CODE&state=STATE&iss=https%3A%2F%2Fapi.flavoso.com

# Not connected, for example after "Cancel"
https://example.com/flavoso/callback?error=access_denied&error_description=...&state=STATE&iss=https%3A%2F%2Fapi.flavoso.com

Die Rückkehr

Nach „Verbinden“ leitet Flavoso mit code, state und iss auf deine redirect_uri zurück. Prüfe, dass state dem mitgeschickten Wert entspricht und iss genau https://api.flavoso.com ist, der issuer aus den Metadaten (auch wenn die Zustimmung auf app.flavoso.com stattfand). Erst dann tauschst du den Code.

Der Code gilt 10 Minuten und genau einmal. Wird er ein zweites Mal eingelöst, widerruft Flavoso alles, was mit ihm ausgegeben wurde.

Bei unbekannter client_id, nicht eingetragener redirect_uri oder doppelten Parametern zeigt Flavoso eine Fehlerseite und leitet nicht weiter. Die übrigen Fehler kommen als Weiterleitung an die redirect_uri, mit error, error_description, state und iss, aber nur bei einer freigegebenen App oder wenn du als ihr Entwickler angemeldet bist. Sonst zeigt Flavoso den Fehler selbst an; eine abgelehnte oder gesperrte App bekommt nie eine Weiterleitung. Diese Werte kann error haben:

errorBedeutung
access_deniedDer Nutzer hat „Abbrechen“ gedrückt, oder er darf diese App nicht mit dem gewählten Betrieb verbinden: etwa, weil er dort nicht Inhaber ist, oder weil eine vertrauliche App über Loopback oder ein eigenes Schema nur deinen Test-Betrieb verbindet.
invalid_requestEin Parameter fehlt oder ist ungültig, etwa state oder die code_challenge.
invalid_scopeEin angefragtes Recht gibt es nicht, oder deine App darf es nicht anfragen.
unsupported_response_typeresponse_type ist nicht code.
server_errorBei Flavoso ist etwas schiefgegangen, etwa weil sich die Verbindung nicht speichern ließ. Später noch einmal versuchen.

Code gegen Tokens tauschen

Den Code tauschst du per POST an https://api.flavoso.com/oauth/token, Rumpf als application/x-www-form-urlencoded: grant_type=authorization_code mit code, redirect_uri (dieselbe wie bei der Autorisierung) und code_verifier. Vertrauliche Clients melden sich per HTTP Basic an, öffentliche schicken client_id.

# Confidential client: HTTP Basic (or client_id and client_secret in the body)
curl https://api.flavoso.com/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=AUTHORIZATION_CODE \
  --data-urlencode redirect_uri=https://example.com/flavoso/callback \
  -d code_verifier=$CODE_VERIFIER

# Public client (native or desktop app): client_id only, no secret
curl https://api.flavoso.com/oauth/token \
  -d grant_type=authorization_code \
  -d code=AUTHORIZATION_CODE \
  --data-urlencode redirect_uri=com.example.pos:/callback \
  -d code_verifier=$CODE_VERIFIER \
  -d client_id=$CLIENT_ID

Die Antwort enthält beide Tokens, die erteilten Rechte, die Kennung der Verbindung und den Betrieb:

Antwort
{
  "access_token": "flv_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "flv_rt_...",
  "scope": "orders:write menu:write store:write reports:read",
  "connection_id": "4b3f7c1e-9a2d-4e8b-b5f6-0c1d2e3f4a5b",
  "tenant": {
    "id": "7d0e5b8a-3c41-4f2e-9a6b-1e2d3c4b5a69",
    "name": "Pizzeria Example",
    "slug": "pizzeria-example"
  }
}

Speichere die Tokens je Verbindung, etwa unter tenant.id. Ein Token gilt für genau einen Betrieb; jeder weitere Betrieb bekommt seine eigene Verbindung mit eigenen Tokens.

Tokens und Lebensdauer

WasFormGilt
Autorisierungscodeflv_ac_…10 Minuten, genau einmal
Access-Tokenflv_at_…1 Stunde (expires_in: 3600)
Refresh-Tokenflv_rt_…180 Tage ab der letzten Nutzung, bei jeder Nutzung getauscht
Client-Secretflv_cs_…bis zum nächsten „Neues Client-Secret erzeugen“, das alte dann noch 24 Stunden

Behandle Tokens wie Passwörter: verschlüsselt speichern, nie in Protokolle schreiben, nie in einen Browser oder eine Adresse.

Erneuern mit Rotation

Ein neues Access-Token holst du mit grant_type=refresh_token und refresh_token. Die Antwort bringt auch ein neues Refresh-Token; das alte ist damit verbraucht. Speichere das neue, bevor du das neue Access-Token benutzt.

Geht die Antwort unterwegs verloren und schickst du dasselbe Refresh-Token innerhalb von 60 Sekunden noch einmal, bekommst du ein neues Paar, aber nur, solange das Refresh-Token aus der verlorenen Antwort noch unbenutzt ist. Das Paar aus der verlorenen Antwort widerruft Flavoso dabei. In jedem anderen Fall gilt ein zweites Einlösen als Diebstahl: Flavoso widerruft die ganze Familie, also alle Tokens aus derselben Zustimmung, und der Inhaber muss neu verbinden.

Erneuere je Verbindung nur an einer Stelle zur selben Zeit. Zwei Prozesse, die gleichzeitig erneuern, sind der häufigste Grund für eine verlorene Verbindung.

Mit scope engst du das neue Access-Token auf weniger Rechte ein, etwa für einen Teil deines Systems, der nur liest. Das Refresh-Token behält alle Rechte der Verbindung.

// A valid access token for one connection. tokenRequest() as above.
// Refresh at one place at a time per connection, and always store the NEW refresh token.
export async function accessTokenFor(conn) {
  if (Date.now() < conn.expiresAt - 60_000) return conn.accessToken;
  try {
    const t = await tokenRequest({ grant_type: "refresh_token", refresh_token: conn.refreshToken });
    conn.accessToken = t.access_token;
    conn.refreshToken = t.refresh_token; // the old one is used up
    conn.expiresAt = Date.now() + t.expires_in * 1000;
    await saveConnection(conn); // persist BEFORE using the new token
    return conn.accessToken;
  } catch (err) {
    if (err.code === "invalid_grant") await markDisconnected(conn); // the owner has to connect again
    if (err.code === "unauthorized_client") await markSuspended(conn); // app suspended: keep the tokens, retry later
    throw err;
  }
}

Widerrufen und trennen

POST https://api.flavoso.com/oauth/revoke mit token widerruft ein Token (RFC 7009); token_type_hint darfst du mitschicken, Flavoso erkennt die Art aber an der Form. Der Client meldet sich dabei an wie am Token-Endpunkt. Fehlt token, antwortet Flavoso mit 400 invalid_request; scheitert die Anmeldung, mit 401 invalid_client. Sonst ist die Antwort immer 200, auch für ein unbekanntes oder schon widerrufenes Token. Ein Refresh-Token nimmt seine ganze Familie mit, ein Access-Token nur sich selbst. Die Verbindung bleibt bestehen.

Um die Verbindung zum Betrieb zu beenden, ruft deine App DELETE /connection. Dann gilt kein Token dieser Verbindung mehr, die Inhaber bekommen eine E-Mail, und dein Webhook bekommt app.disconnected. Für dieselbe App und denselben Betrieb braucht es danach eine neue Zustimmung.

# Revoke a token (RFC 7009). The client authenticates as at the token endpoint;
# after that the answer is always 200. A refresh token takes its whole family with it.
curl https://api.flavoso.com/oauth/revoke \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d token=$REFRESH_TOKEN \
  -d token_type_hint=refresh_token

# End the connection to the restaurant: every token of it stops working, the owners are notified
curl -X DELETE https://api.flavoso.com/v1/connection \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Metadaten

Adressen und unterstützte Verfahren stehen maschinenlesbar unter https://api.flavoso.com/.well-known/oauth-authorization-server (RFC 8414). Viele OAuth-Bibliotheken lesen sie selbst ein.

curl https://api.flavoso.com/.well-known/oauth-authorization-server

Fehler am Token-Endpunkt

Der Token-Endpunkt antwortet nach RFC 6749 mit error und error_description, jede Antwort mit Cache-Control: no-store:

errorHTTPBedeutung
invalid_request400Ein Parameter fehlt, ist doppelt oder ungültig.
invalid_client401Die Anmeldung des Clients ist gescheitert, mit WWW-Authenticate.
invalid_grant400Code oder Refresh-Token ist ungültig, abgelaufen, schon benutzt oder widerrufen, oder redirect_uri oder code_verifier passt nicht. Beim Erneuern heißt das: die Verbindung besteht nicht mehr.
unauthorized_client400Die App ist gesperrt. Die Verbindungen bestehen weiter: Tokens behalten und später erneut versuchen.
unsupported_grant_type400Diesen grant_type gibt es nicht.
invalid_scope400Ein Recht ist ungültig oder geht über die Verbindung hinaus.
temporarily_unavailable429Zu viele Anfragen; Retry-After nennt die Wartezeit in Sekunden.
server_error500Bei Flavoso ist etwas schiefgegangen.
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid, expired or already used."
}

Der Knopf „Mit Flavoso verbinden“

In deiner Software startet ein Knopf den Ablauf. Er führt auf deinen Server, der state und PKCE erzeugt und an Flavoso weiterleitet. Der Knopf trägt den Namen Flavoso unverändert und keine Aussage, die Flavoso nicht gemacht hat:

SpracheAufschrift
DeutschMit Flavoso verbinden
EnglischConnect with Flavoso
FranzösischConnecter avec Flavoso
SpanischConectar con Flavoso
ItalienischCollega con Flavoso
<!-- The link goes to YOUR server, which creates state and PKCE and redirects to Flavoso -->
<a class="flavoso-connect" href="/integrations/flavoso/connect">Connect with Flavoso</a>

<style>
  .flavoso-connect {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.625rem 1rem;
    border-radius: 0.75rem;
    background: #ff5a1f;
    color: #ffffff;
    font: 600 15px/1.2 system-ui, sans-serif;
    text-decoration: none;
  }
  .flavoso-connect:hover {
    background: #e6440c;
  }
  .flavoso-connect:focus-visible {
    outline: 3px solid #ffb38f;
    outline-offset: 2px;
  }
</style>

Ist die Verbindung da, zeig in deiner Software, mit welchem Betrieb (tenant.name) und seit wann, und biete „Trennen“ an (DELETE /connection). Weitere Regeln zur Marke stehen unter Freigabe.

Viele Betriebe

Eine Kasse mit vielen Kunden hat viele Verbindungen. Jede hat eigene Tokens, eine eigene Anfragegrenze und ihre connection_id. Ein Webhook deiner App bekommt die Ereignisse aller Betriebe; tenant_id und connection_id im Umschlag sagen, zu welchem es gehört.

Ein Betrieb, der dich ein zweites Mal verbindet, behält dieselbe Verbindung mit neuen Rechten. Nach einer Trennung entsteht beim nächsten Verbinden eine neue Verbindung mit neuer connection_id.

Etwas unklar geblieben? Schreib an support@flavoso.com und nenne die Kennung aus X-Request-Id. Damit finden wir deine Anfrage im Protokoll.