API de FlavosoFundamentosAutenticación

Autenticación

OAuth 2.0 con PKCE: cómo un propietario conecta tu app con su negocio y cómo intercambias, renuevas y revocas tokens.

Visión general

Una app solo obtiene acceso a un negocio con el consentimiento de su propietario. Para ello, Flavoso usa OAuth 2.0 con código de autorización (RFC 6749) y PKCE con S256 (RFC 7636), además del parámetro iss en la redirección de vuelta (RFC 9207), la revocación según RFC 7009 y los metadatos según RFC 8414.

  1. 1

    Tu app

    «Conectar con Flavoso»

    Tu app genera state y code_verifier y redirige el navegador a /oauth/authorize.

  2. 2

    Flavoso

    Iniciar sesión

    El propietario inicia sesión con su cuenta de Flavoso, con un segundo factor si lo ha configurado.

  3. 3

    Flavoso

    Aceptar

    Elige el negocio, lee el nombre, la web y los permisos de tu app y pulsa «Conectar».

  4. 4

    Navegador

    Vuelta con code

    Flavoso redirige de vuelta a tu redirect_uri con code, state e iss. El code es válido durante 10 minutos y solo se puede usar una vez.

  5. 5

    Tu servidor

    Intercambiar

    Tu servidor intercambia code y code_verifier por un token de acceso y un token de actualización.

  6. 6

    Tu servidor

    Solicitudes

    Llamadas a la API con el token de acceso (1 hora); el token de actualización obtiene tokens nuevos.

EndpointDirección
Autorización (navegador)https://app.flavoso.com/oauth/authorize
TokenPOST https://api.flavoso.com/oauth/token
RevocaciónPOST https://api.flavoso.com/oauth/revoke
Metadatoshttps://api.flavoso.com/.well-known/oauth-authorization-server

La autorización es una página en app.flavoso.com, porque allí inicia sesión el propietario. El token, la revocación y los metadatos están en https://api.flavoso.com. Los endpoints con todos sus parámetros también están en la referencia.

Clientes confidenciales y públicos

El tipo de cliente lo eliges al crear la app:

  • Confidencial (confidential): para apps con servidor propio. Reciben un client secret (flv_cs_…), que se muestra exactamente una vez y solo debe estar en el servidor. En el endpoint de token se autentican con HTTP Basic o envían client_id y client_secret en el cuerpo. Los negocios reales solo las conectan a través de una URI de redirección https.
  • Público (public): para apps que no pueden guardar un secreto de forma segura, por ejemplo una app de caja en una tableta o un programa en el ordenador del local. No reciben ningún secret y solo envían client_id. Pueden usar direcciones loopback y un esquema propio (RFC 8252), también con negocios reales.

PKCE con S256 es obligatorio para ambos. El endpoint de token no acepta llamadas desde el navegador (sin CORS); por eso, una app web intercambia el código y el token de actualización en su servidor, lo que la convierte en un cliente confidencial.

El client ID (flv_app_…) no es secreto. «Crear un nuevo client secret» en la consola sustituye el client secret; el anterior sigue siendo válido 24 horas, así que puedes cambiar sin interrupciones.

URI de redirección

Tras el consentimiento, Flavoso devuelve el navegador a una de las URI de redirección de tu app. Estas son las reglas:

  • Flavoso compara carácter a carácter con las direcciones registradas. La única excepción es el puerto en las direcciones loopback.
  • https, sin credenciales en la dirección. Es la vía por la que los negocios reales conectan cualquier app.
  • Loopback (http://127.0.0.1:<port>/…, http://localhost:<port>/…, http://[::1]:<port>/…, cualquier puerto, RFC 8252) y esquemas propios con punto (com.example.pos:/callback): en una app pública con cualquier negocio, en una app confidencial solo con tu propio negocio de prueba. En ese caso, la pantalla de consentimiento solo ofrece ese negocio.
  • Sin fragmento (#), como máximo 300 caracteres y como máximo 10 direcciones por app.

Solicitar la autorización

Tu app redirige el navegador a https://app.flavoso.com/oauth/authorize con estos parámetros:

ParámetroObligatorioSignificado
response_typesíSiempre code.
client_idsíEl client ID de tu app.
redirect_urisíUna de las direcciones registradas, exactamente como está registrada.
scopenoLos permisos que quieres, separados por espacios; solo pueden ser permisos de tu app. Sin scope, solicitas todos los permisos de tu app.
statesíUn valor aleatorio (hasta 500 caracteres) que guardas y compruebas a la vuelta. Protege contra respuestas falsificadas.
code_challengesíBASE64URL(SHA-256(code_verifier)) sin = al final. El code_verifier (de 43 a 128 caracteres) lo conservas hasta el intercambio.
code_challenge_methodsíSiempre 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

La dirección redirige a la versión en el idioma del navegador. Así son la solicitud y la redirección de vuelta:

# 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

La redirección de vuelta

Tras «Conectar», Flavoso redirige de vuelta a tu redirect_uri con code, state e iss. Comprueba que state coincide con el valor enviado y que iss es exactamente https://api.flavoso.com, el issuer de los metadatos (aunque el consentimiento haya tenido lugar en app.flavoso.com). Solo entonces intercambias el código.

El código es válido durante 10 minutos y solo se puede canjear una vez. Si se canjea por segunda vez, Flavoso revoca todo lo que se emitió con él.

Si el client_id es desconocido, la redirect_uri no está registrada o hay parámetros repetidos, Flavoso muestra una página de error y no redirige. Los demás errores llegan como redirección a la redirect_uri, con error, error_description, state e iss, pero solo en una app aprobada o si has iniciado sesión como su desarrollador. En caso contrario, Flavoso muestra el error en su propia página; una app rechazada o bloqueada nunca recibe una redirección. error puede tener estos valores:

errorSignificado
access_deniedEl usuario ha pulsado «Cancelar», o no puede conectar esta app con el negocio elegido: por ejemplo, porque no es su propietario, o porque una app confidencial solo conecta tu negocio de prueba a través de una dirección loopback o un esquema propio.
invalid_requestFalta un parámetro o no es válido, por ejemplo state o el code_challenge.
invalid_scopeUn permiso solicitado no existe, o tu app no puede solicitarlo.
unsupported_response_typeresponse_type no es code.
server_errorAlgo ha fallado en Flavoso, por ejemplo, no se ha podido guardar la conexión. Vuelve a intentarlo más tarde.

Intercambiar el código por tokens

El código lo intercambias con un POST a https://api.flavoso.com/oauth/token, con el cuerpo en application/x-www-form-urlencoded: grant_type=authorization_code con code, redirect_uri (la misma que en la solicitud de autorización) y code_verifier. Los clientes confidenciales se autentican con HTTP Basic; los públicos envían 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 respuesta contiene ambos tokens, los permisos concedidos, el identificador de la conexión y el negocio:

Respuesta
{
  "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"
  }
}

Guarda los tokens de cada conexión, por ejemplo asociados a tenant.id. Un token solo es válido para un negocio; cada negocio adicional tiene su propia conexión con sus propios tokens.

Tokens y duración

QuéFormatoValidez
Código de autorizaciónflv_ac_…10 minutos, un solo uso
Token de accesoflv_at_…1 hora (expires_in: 3600)
Token de actualizaciónflv_rt_…180 días desde el último uso; se sustituye en cada uso
Client secretflv_cs_…hasta el siguiente «Crear un nuevo client secret»; el anterior, 24 horas más

Trata los tokens como contraseñas: guárdalos cifrados, nunca los escribas en registros y nunca los pongas en un navegador ni en una dirección.

Renovar con rotación

Para obtener un nuevo token de acceso, envía grant_type=refresh_token y refresh_token. La respuesta incluye también un nuevo token de actualización; el anterior deja de servir. Guarda el nuevo antes de usar el nuevo token de acceso.

Si la respuesta se pierde por el camino y vuelves a enviar el mismo token de actualización en un plazo de 60 segundos, recibes un par nuevo, pero solo mientras el token de actualización de la respuesta perdida siga sin usarse. Flavoso revoca entonces el par de la respuesta perdida. En cualquier otro caso, un segundo canje se considera un robo: Flavoso revoca toda la familia, es decir, todos los tokens del mismo consentimiento, y el propietario tiene que volver a conectar.

Para cada conexión, renueva los tokens desde un solo sitio a la vez. Dos procesos que renuevan al mismo tiempo son la causa más frecuente de perder una conexión.

Con scope limitas el nuevo token de acceso a menos permisos, por ejemplo para una parte de tu sistema que solo lee. El token de actualización conserva todos los permisos de la conexión.

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

Revocar y desconectar

POST https://api.flavoso.com/oauth/revoke con token revoca un token (RFC 7009); puedes enviar token_type_hint, pero Flavoso reconoce el tipo por su formato. El cliente se autentica igual que en el endpoint de token. Si falta token, Flavoso responde con 400 invalid_request; si la autenticación falla, con 401 invalid_client. En los demás casos, la respuesta es siempre 200, incluso para un token desconocido o ya revocado. Revocar un token de actualización revoca toda su familia; revocar un token de acceso, solo ese token. La conexión en sí se mantiene.

Para terminar la conexión con el negocio, tu app llama a DELETE /connection. A partir de ese momento, ningún token de esa conexión es válido, los propietarios reciben un e-mail y tu webhook recibe app.disconnected. Después, para conectar la misma app con el mismo negocio, hace falta un nuevo consentimiento.

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

Metadatos

Las direcciones y los métodos admitidos están, en formato legible por máquina, en https://api.flavoso.com/.well-known/oauth-authorization-server (RFC 8414). Muchas bibliotecas de OAuth los leen automáticamente.

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

Errores en el endpoint de token

El endpoint de token responde según RFC 6749 con error y error_description, y cada respuesta lleva Cache-Control: no-store:

errorHTTPSignificado
invalid_request400Falta un parámetro, está repetido o no es válido.
invalid_client401La autenticación del cliente ha fallado; la respuesta lleva WWW-Authenticate.
invalid_grant400El código o el token de actualización no es válido, ha caducado, ya se ha usado o se ha revocado, o no coinciden la redirect_uri o el code_verifier. Al renovar, significa que la conexión ya no existe.
unauthorized_client400La app está bloqueada. Sus conexiones se mantienen: conserva los tokens y vuelve a intentarlo más tarde.
unsupported_grant_type400Este grant_type no existe.
invalid_scope400Un permiso no es válido o va más allá de la conexión.
temporarily_unavailable429Demasiadas solicitudes; Retry-After indica la espera en segundos.
server_error500Algo ha fallado en Flavoso.
{
  "error": "invalid_grant",
  "error_description": "The authorization code is invalid, expired or already used."
}

El botón «Conectar con Flavoso»

En tu software, un botón inicia el flujo. Lleva a tu servidor, que genera state y PKCE y redirige a Flavoso. El botón muestra el nombre Flavoso sin modificar y no hace ninguna afirmación que Flavoso no haya hecho:

IdiomaTexto
AlemánMit Flavoso verbinden
InglésConnect with Flavoso
FrancésConnecter avec Flavoso
EspañolConectar con Flavoso
ItalianoCollega 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>

Una vez establecida la conexión, muestra en tu software con qué negocio está conectado (tenant.name) y desde cuándo, y ofrece la opción «Desconectar» (DELETE /connection). Encontrarás más reglas sobre la marca en Revisión.

Muchos negocios

Un sistema de caja con muchos clientes tiene muchas conexiones. Cada una tiene sus propios tokens, su propio límite de solicitudes y su connection_id. El webhook de tu app recibe los eventos de todos los negocios; tenant_id y connection_id en el sobre indican a cuál pertenece cada uno.

Un negocio que conecta tu app por segunda vez conserva la misma conexión con permisos nuevos. Tras una desconexión, al volver a conectar se crea una conexión nueva con un connection_id nuevo.

¿Algo no ha quedado claro? Escribe a support@flavoso.com e indica el identificador de X-Request-Id. Así podremos encontrar tu solicitud en el registro.