API FlavosoFondamentauxAuthentification

Authentification

OAuth 2.0 avec PKCE : comment un propriétaire connecte votre application à son établissement, et comment vous échangez, rafraîchissez et révoquez les jetons.

Vue d'ensemble

Une application n'obtient accès à un établissement qu'avec le consentement de son propriétaire. Flavoso utilise pour cela OAuth 2.0 avec code d'autorisation (RFC 6749) et PKCE avec S256 (RFC 7636), ainsi que le paramètre iss au retour (RFC 9207), la révocation selon la RFC 7009 et les métadonnées selon la RFC 8414.

  1. 1

    Votre application

    « Connecter avec Flavoso »

    Votre application crée state et un code_verifier et envoie le navigateur vers /oauth/authorize.

  2. 2

    Flavoso

    S'identifier

    Le propriétaire s'identifie avec son compte Flavoso, avec un second facteur s'il en a configuré un.

  3. 3

    Flavoso

    Consentement

    Il choisit l'établissement, lit le nom, le site web et les droits de votre application et clique sur « Connecter ».

  4. 4

    Navigateur

    Retour avec un code

    Flavoso redirige vers votre redirect_uri avec code, state et iss. Le code est valable 10 minutes, et une seule fois.

  5. 5

    Votre serveur

    Échange

    Votre serveur échange code et code_verifier contre un jeton d'accès et un jeton de rafraîchissement.

  6. 6

    Votre serveur

    Requêtes

    Appelez l'API avec le jeton d'accès (1 heure) ; le jeton de rafraîchissement permet d'en obtenir de nouveaux.

Point de terminaisonAdresse
Autorisation (navigateur)https://app.flavoso.com/oauth/authorize
JetonPOST https://api.flavoso.com/oauth/token
RévocationPOST https://api.flavoso.com/oauth/revoke
Métadonnéeshttps://api.flavoso.com/.well-known/oauth-authorization-server

L'autorisation est une page sur app.flavoso.com, car c'est là que le propriétaire s'identifie. Le jeton, la révocation et les métadonnées se trouvent sur https://api.flavoso.com. Les points de terminaison et tous leurs paramètres figurent aussi dans la référence.

Clients confidentiels et publics

Vous choisissez le type de client lors de la création de l'application :

  • Confidentiel (confidential) : pour les applications dotées de leur propre serveur. Elles reçoivent un client secret (flv_cs_…), qui ne s'affiche qu'une seule fois et n'a sa place que sur le serveur. Auprès du point de terminaison des jetons, elles s'authentifient par HTTP Basic ou envoient client_id et client_secret dans le corps. Les vrais établissements ne les connectent que via une URI de redirection https.
  • Public (public) : pour les applications qui ne peuvent pas garder un secret en sécurité, par exemple une application de caisse sur une tablette ou un programme sur l'ordinateur de l'établissement. Elles ne reçoivent pas de secret et n'envoient que client_id. Elles peuvent utiliser des adresses loopback et un schéma personnalisé (RFC 8252), y compris pour de vrais établissements.

PKCE avec S256 est obligatoire pour les deux. Le point de terminaison des jetons n'accepte aucun appel depuis un navigateur (pas de CORS) ; une application web échange donc le code et le jeton de rafraîchissement sur son serveur, ce qui en fait un client confidentiel.

Le client ID (flv_app_…) n'est pas secret. « Créer un nouveau client secret » dans la console remplace le client secret ; l'ancien reste valable 24 heures, le temps de basculer sans interruption.

URI de redirection

Après le consentement, Flavoso renvoie le navigateur vers l'une des URI de redirection de votre application. Les règles sont les suivantes :

  • Flavoso compare l'adresse caractère par caractère avec les adresses enregistrées. Seule exception : le port des adresses loopback.
  • https, sans identifiants dans l'adresse. Les vrais établissements connectent toute application par ce biais.
  • Loopback (http://127.0.0.1:<port>/…, http://localhost:<port>/…, http://[::1]:<port>/…, n'importe quel port, RFC 8252) et schémas personnalisés contenant un point (com.example.pos:/callback) : pour une application publique, avec n'importe quel établissement ; pour une application confidentielle, uniquement avec votre propre établissement de test. L'écran de consentement ne propose alors que celui-ci.
  • Pas de fragment (#), 300 caractères au maximum, 10 adresses au maximum par application.

Demander l'autorisation

Votre application redirige le navigateur vers https://app.flavoso.com/oauth/authorize, avec les paramètres suivants :

ParamètreObligatoireSignification
response_typeouiToujours code.
client_idouiLe client ID de votre application.
redirect_uriouiL'une des adresses enregistrées, écrite à l'identique.
scopenonLes droits demandés, séparés par des espaces, uniquement parmi les droits de votre application. Sans scope, vous demandez tous les droits de votre application.
stateouiUne valeur aléatoire (jusqu'à 500 caractères) que vous conservez et comparez après le retour. Protège contre les réponses falsifiées.
code_challengeouiBASE64URL(SHA-256(code_verifier)) sans = à la fin. Vous conservez le code_verifier (43 à 128 caractères) jusqu'à l'échange.
code_challenge_methodouiToujours 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

L'adresse redirige vers la version dans la langue du navigateur. Voici à quoi ressemblent la requête et le retour :

# 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

Le retour

Après « Connecter », Flavoso redirige vers votre redirect_uri avec code, state et iss. Vérifiez que state correspond à la valeur envoyée et que iss vaut exactement https://api.flavoso.com, l'issuer des métadonnées (même si le consentement a eu lieu sur app.flavoso.com). Ensuite seulement, échangez le code.

Le code est valable 10 minutes et une seule fois. S'il est utilisé une seconde fois, Flavoso révoque tout ce qui a été délivré avec lui.

Avec un client_id inconnu, une redirect_uri non enregistrée ou des paramètres en double, Flavoso affiche une page d'erreur et ne redirige pas. Les autres erreurs reviennent sous forme de redirection vers la redirect_uri, avec error, error_description, state et iss, mais seulement pour une application validée ou si vous êtes identifié comme son développeur. Sinon, Flavoso affiche lui-même l'erreur ; une application refusée ou bloquée n'obtient jamais de redirection. error peut prendre ces valeurs :

errorSignification
access_deniedL'utilisateur a cliqué sur « Annuler », ou il n'a pas le droit de connecter cette application à l'établissement choisi : par exemple parce qu'il n'en est pas propriétaire, ou parce qu'une application confidentielle ne peut se connecter qu'à votre établissement de test via une adresse loopback ou un schéma personnalisé.
invalid_requestUn paramètre manque ou est invalide, par exemple state ou le code_challenge.
invalid_scopeUn droit demandé n'existe pas, ou votre application n'a pas le droit de le demander.
unsupported_response_typeresponse_type n'est pas code.
server_errorUn problème est survenu du côté de Flavoso, par exemple la connexion n'a pas pu être enregistrée. Réessayez plus tard.

Échanger le code contre des jetons

Pour échanger le code, envoyez un POST à https://api.flavoso.com/oauth/token, avec un corps en application/x-www-form-urlencoded : grant_type=authorization_code avec code, redirect_uri (la même que lors de l'autorisation) et code_verifier. Les clients confidentiels s'authentifient par HTTP Basic, les clients publics envoient 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

La réponse contient les deux jetons, les droits accordés, l'identifiant de la connexion et l'établissement :

Réponse
{
  "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"
  }
}

Enregistrez les jetons par connexion, par exemple sous tenant.id. Un jeton n'est valable que pour un seul établissement ; chaque établissement supplémentaire obtient sa propre connexion avec ses propres jetons.

Jetons et durées de validité

ÉlémentFormatValidité
Code d'autorisationflv_ac_…10 minutes, une seule fois
Jeton d'accèsflv_at_…1 heure (expires_in: 3600)
Jeton de rafraîchissementflv_rt_…180 jours à compter de la dernière utilisation, remplacé à chaque utilisation
Client secretflv_cs_…jusqu'au prochain « Créer un nouveau client secret », l'ancien reste alors valable 24 heures

Traitez les jetons comme des mots de passe : enregistrez-les chiffrés, ne les écrivez jamais dans des journaux, ne les placez jamais dans un navigateur ni dans une adresse.

Rafraîchir avec rotation

Vous obtenez un nouveau jeton d'accès avec grant_type=refresh_token et refresh_token. La réponse contient aussi un nouveau jeton de rafraîchissement ; l'ancien est alors consommé. Enregistrez le nouveau avant d'utiliser le nouveau jeton d'accès.

Si la réponse se perd en route et que vous renvoyez le même jeton de rafraîchissement dans les 60 secondes, vous obtenez une nouvelle paire, mais seulement tant que le jeton de rafraîchissement de la réponse perdue n'a pas encore servi. Flavoso révoque alors la paire de la réponse perdue. Dans tous les autres cas, une seconde utilisation est considérée comme un vol : Flavoso révoque toute la famille, c'est-à-dire tous les jetons issus du même consentement, et le propriétaire doit connecter l'application à nouveau.

Ne rafraîchissez qu'à un seul endroit à la fois par connexion. Deux processus qui rafraîchissent en même temps sont la cause la plus fréquente d'une connexion perdue.

Avec scope, vous restreignez le nouveau jeton d'accès à moins de droits, par exemple pour une partie de votre système qui ne fait que lire. Le jeton de rafraîchissement conserve tous les droits de la connexion.

// 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;
  }
}

Révoquer et déconnecter

POST https://api.flavoso.com/oauth/revoke avec token révoque un jeton (RFC 7009) ; vous pouvez envoyer token_type_hint, mais Flavoso reconnaît le type à la forme du jeton. Le client s'authentifie comme auprès du point de terminaison des jetons. Sans token, Flavoso répond 400 invalid_request ; si l'authentification échoue, 401 invalid_client. Sinon, la réponse est toujours 200, y compris pour un jeton inconnu ou déjà révoqué. Révoquer un jeton de rafraîchissement révoque toute sa famille ; un jeton d'accès, seulement lui-même. La connexion subsiste.

Pour mettre fin à la connexion avec l'établissement, votre application appelle DELETE /connection. Plus aucun jeton de cette connexion ne fonctionne alors, les propriétaires reçoivent un e-mail, et votre webhook reçoit app.disconnected. Pour la même application et le même établissement, un nouveau consentement est ensuite nécessaire.

# 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"

Métadonnées

Les adresses et les méthodes prises en charge sont disponibles sous une forme lisible par machine à https://api.flavoso.com/.well-known/oauth-authorization-server (RFC 8414). De nombreuses bibliothèques OAuth les lisent automatiquement.

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

Erreurs du point de terminaison des jetons

Le point de terminaison des jetons répond selon la RFC 6749 avec error et error_description, toujours avec Cache-Control: no-store :

errorHTTPSignification
invalid_request400Un paramètre manque, est répété ou est invalide.
invalid_client401L'authentification du client a échoué, avec WWW-Authenticate.
invalid_grant400Le code ou le jeton de rafraîchissement est invalide, expiré, déjà utilisé ou révoqué, ou redirect_uri ou code_verifier ne correspond pas. Lors d'un rafraîchissement, cela signifie : la connexion n'existe plus.
unauthorized_client400L'application est bloquée. Ses connexions subsistent : conservez les jetons et réessayez plus tard.
unsupported_grant_type400Ce grant_type n'existe pas.
invalid_scope400Un droit est invalide ou dépasse ceux de la connexion.
temporarily_unavailable429Trop de requêtes ; Retry-After indique le délai d'attente en secondes.
server_error500Un problème est survenu du côté de Flavoso.
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid, expired or already used."
}

Le bouton « Connecter avec Flavoso »

Dans votre logiciel, un bouton lance le parcours. Il mène à votre serveur, qui crée state et PKCE et redirige vers Flavoso. Le bouton reprend le nom Flavoso sans le modifier et ne comporte aucune affirmation que Flavoso n'a pas faite :

LangueLibellé
AllemandMit Flavoso verbinden
AnglaisConnect with Flavoso
FrançaisConnecter avec Flavoso
EspagnolConectar con Flavoso
ItalienCollega 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>

Une fois la connexion établie, affichez dans votre logiciel l'établissement connecté (tenant.name) et depuis quand, et proposez « Déconnecter » (DELETE /connection). D'autres règles concernant la marque figurent sous Validation.

Plusieurs établissements

Un système de caisse utilisé par de nombreux établissements a de nombreuses connexions. Chacune a ses propres jetons, sa propre limite de requêtes et son connection_id. Le webhook de votre application reçoit les événements de tous les établissements ; tenant_id et connection_id dans l'enveloppe indiquent à quel établissement chacun appartient.

Un établissement qui connecte votre application une seconde fois conserve la même connexion, avec les nouveaux droits. Après une déconnexion, la prochaine autorisation crée une nouvelle connexion avec un nouveau connection_id.

Une question reste ouverte ? Écrivez à support@flavoso.com en indiquant l'identifiant de X-Request-Id. Il nous permet de retrouver votre requête dans le journal.