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
Tu app
«Conectar con Flavoso»
Tu app genera state y code_verifier y redirige el navegador a /oauth/authorize.
- 2
Flavoso
Iniciar sesión
El propietario inicia sesión con su cuenta de Flavoso, con un segundo factor si lo ha configurado.
- 3
Flavoso
Aceptar
Elige el negocio, lee el nombre, la web y los permisos de tu app y pulsa «Conectar».
- 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
Tu servidor
Intercambiar
Tu servidor intercambia code y code_verifier por un token de acceso y un token de actualización.
- 6
Tu servidor
Solicitudes
Llamadas a la API con el token de acceso (1 hora); el token de actualización obtiene tokens nuevos.
| Endpoint | Dirección |
|---|---|
| Autorización (navegador) | https://app.flavoso.com/oauth/authorize |
| Token | POST https://api.flavoso.com/oauth/token |
| Revocación | POST https://api.flavoso.com/oauth/revoke |
| Metadatos | https://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íanclient_idyclient_secreten el cuerpo. Los negocios reales solo las conectan a través de una URI de redirecciónhttps. - 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íanclient_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.
El consentimiento
En la pantalla de consentimiento, el usuario inicia sesión con su cuenta de Flavoso; si ha configurado un segundo factor, se le pide. Después elige el negocio, ve el logo, el nombre y la web de tu app y los permisos solicitados en lenguaje claro, y pulsa «Conectar» o «Cancelar». Las apps aprobadas muestran ahí «Revisada por Flavoso».
- Solo los propietarios pueden conectar. Quien tiene otro rol en el negocio no lo ve en la lista.
- Antes de la aprobación solo puedes conectar tú: con tu negocio de prueba y con negocios de los que eres propietario. Con una dirección loopback o un esquema propio, una app confidencial solo puede conectar tu negocio de prueba, incluso después de la aprobación.
- Todos los propietarios del negocio reciben un e-mail tras la conexión: qué app, qué proveedor, qué permisos, quién y cuándo. Lo mismo ocurre cuando una conexión existente recibe más permisos.
- Volver a conectar sustituye los permisos de la conexión existente, sean más o menos. Tu app recibe entonces
app.scopes_changedcon los permisos anteriores.
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:
| error | Significado |
|---|---|
access_denied | El 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_request | Falta un parámetro o no es válido, por ejemplo state o el code_challenge. |
invalid_scope | Un permiso solicitado no existe, o tu app no puede solicitarlo. |
unsupported_response_type | response_type no es code. |
server_error | Algo 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_IDLa respuesta contiene ambos tokens, los permisos concedidos, el identificador de la conexión y el negocio:
{
"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é | Formato | Validez |
|---|---|---|
| Código de autorización | flv_ac_… | 10 minutos, un solo uso |
| Token de acceso | flv_at_… | 1 hora (expires_in: 3600) |
| Token de actualización | flv_rt_… | 180 días desde el último uso; se sustituye en cada uso |
| Client secret | flv_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.
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-serverErrores 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:
| error | HTTP | Significado |
|---|---|---|
invalid_request | 400 | Falta un parámetro, está repetido o no es válido. |
invalid_client | 401 | La autenticación del cliente ha fallado; la respuesta lleva WWW-Authenticate. |
invalid_grant | 400 | El 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_client | 400 | La app está bloqueada. Sus conexiones se mantienen: conserva los tokens y vuelve a intentarlo más tarde. |
unsupported_grant_type | 400 | Este grant_type no existe. |
invalid_scope | 400 | Un permiso no es válido o va más allá de la conexión. |
temporarily_unavailable | 429 | Demasiadas solicitudes; Retry-After indica la espera en segundos. |
server_error | 500 | Algo ha fallado en Flavoso. |
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid, expired or already used."
}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.