Authentification
L'API publique et le serveur MCP utilisent OAuth 2.0 (avec les règles d'OAuth 2.1 : PKCE S256 obligatoire, aucun flux implicite, rotation des jetons de rafraîchissement). Le serveur d'autorisation est celui de Tauraco ; son émetteur est https://api.tauraco.io, y compris sous les noms *.tauraco.ai, en ligne depuis le 2026-11-06.
Points de terminaison
| Point | Méthode | Norme | Rôle |
|---|---|---|---|
/oauth/token | POST | RFC 6749 | émission et rafraîchissement des jetons |
/oauth/authorize | GET | RFC 6749, PKCE S256 | début du flux délégué, puis page de consentement |
/oauth/revoke | POST | RFC 7009 | révocation d'un jeton |
/oauth/introspect | POST | RFC 7662 | réservé aux clients internes de Tauraco |
/oauth/register | POST | RFC 7591 | enregistrement dynamique des clients MCP, s'il est ouvert |
/.well-known/oauth-authorization-server | GET | RFC 8414 | métadonnées du serveur d'autorisation |
/.well-known/jwks.json | GET | RFC 7517 | clés publiques de signature |
/.well-known/oauth-protected-resource/public/v1 | GET | RFC 9728 | ressource protégée : API publique |
/.well-known/oauth-protected-resource/mcp | GET | RFC 9728 | ressource protégée : serveur MCP |
Tous sont servis sous https://api.tauraco.io et sont en ligne depuis le 2026-11-05 ; l'enregistrement dynamique est ouvert. Un module fermé répondrait 404.
Client credentials (serveur à serveur)
Pour un logiciel qui agit au nom de l'entreprise, sans utilisateur devant l'écran : logiciel comptable, ERP, intégrateur, établissement financier.
- Authentification du client par HTTP Basic (
client_id:client_secret) ou dans le corps (client_secret_post). grant_type=client_credentials,scope(scopes séparés par des espaces),resource=https://api.tauraco.io/public/v1, ettenantsi plusieurs entreprises ont autorisé l'application.- Réponse :
access_token,token_typeBearer,expires_in600,scopeeffectif. Pas de jeton de rafraîchissement. - L'octroi de ce flux est donné par un administrateur de l'entreprise dans la console Applications connectées, jamais par la page de consentement.
"""Obtenir un jeton ``client_credentials`` et lire les factures, page par page.
Get a ``client_credentials`` token and read invoices page by page.
Bibliothèque standard seule (Python 3.9+). Variables d'environnement :
``TAURACO_CLIENT_ID`` (``tc_live_…``), ``TAURACO_CLIENT_SECRET`` (``tcs_…``, affiché une
seule fois à la création de l'application), et facultativement ``TAURACO_TENANT``
(obligatoire si plusieurs entreprises ont autorisé l'application).
Les adresses par défaut sont les surfaces prévues sous ``api.tauraco.io`` ; elles ne
répondent qu'une fois l'API publique ouverte (voir la page « Versions et changelog »).
"""
from __future__ import annotations
import base64
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Callable, Dict, Iterator, Optional, Tuple
API_BASE = os.environ.get("TAURACO_API_BASE", "https://api.tauraco.io/public/v1")
TOKEN_URL = os.environ.get("TAURACO_TOKEN_URL", "https://api.tauraco.io/oauth/token")
#: Transport : (méthode, url, en-têtes, corps) -> (statut, en-têtes, corps). Remplaçable en test.
Transport = Callable[[str, str, Dict[str, str], Optional[bytes]], Tuple[int, Dict[str, str], bytes]]
class TauracoError(RuntimeError):
"""Erreur de l'API : corps ``application/problem+json`` (RFC 9457) ou erreur OAuth."""
def __init__(self, status: int, body: dict):
self.status = status
self.code = body.get("code") or body.get("error") or "unknown"
self.request_id = body.get("request_id")
super().__init__(f"{status} {self.code}: {body.get('detail') or body.get('error_description') or ''}")
class _NoRedirect(urllib.request.HTTPRedirectHandler):
"""Aucune redirection suivie : urllib renverrait sinon l'en-tête ``Authorization`` (jeton)
à l'hôte de destination. L'API Tauraco ne redirige jamais ; un 3xx est rendu tel quel."""
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
_OPENER = urllib.request.build_opener(_NoRedirect)
def http_transport(method: str, url: str, headers: Dict[str, str],
body: Optional[bytes]) -> Tuple[int, Dict[str, str], bytes]:
request = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with _OPENER.open(request, timeout=30) as response: # noqa: S310 (URL maîtrisée)
return response.status, dict(response.headers.items()), response.read()
except urllib.error.HTTPError as err:
return err.code, dict(err.headers.items()), err.read()
def get_token(client_id: str, client_secret: str, scopes: str, *, tenant: Optional[str] = None,
resource: str = API_BASE, transport: Transport = http_transport) -> dict:
"""``POST /oauth/token`` (``grant_type=client_credentials``), authentification HTTP Basic."""
form = {"grant_type": "client_credentials", "scope": scopes, "resource": resource}
if tenant:
form["tenant"] = tenant
basic = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode("ascii")
status, _headers, raw = transport("POST", TOKEN_URL, {
"Authorization": f"Basic {basic}",
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
}, urllib.parse.urlencode(form).encode())
payload = json.loads(raw or b"{}")
if status != 200:
raise TauracoError(status, payload)
return payload # {"access_token", "token_type": "Bearer", "expires_in": 600, "scope"}
def iterate(path: str, token: str, params: Optional[Dict[str, str]] = None, *,
transport: Transport = http_transport, max_retries: int = 3) -> Iterator[dict]:
"""Toutes les lignes d'une liste paginée par curseur (``data``, ``next_cursor``, ``has_more``)."""
query = dict(params or {})
retries = 0
while True:
url = f"{API_BASE}{path}?{urllib.parse.urlencode(query)}" if query else f"{API_BASE}{path}"
status, headers, raw = transport("GET", url, {"Authorization": f"Bearer {token}",
"Accept": "application/json"}, None)
if status == 429 and retries < max_retries:
retries += 1
time.sleep(int(headers.get("Retry-After") or headers.get("retry-after") or 1))
continue
payload = json.loads(raw or b"{}")
if status != 200:
raise TauracoError(status, payload)
retries = 0
yield from payload.get("data", [])
if not payload.get("has_more") or not payload.get("next_cursor"):
return
query["cursor"] = payload["next_cursor"]
def main() -> None:
token = get_token(os.environ["TAURACO_CLIENT_ID"], os.environ["TAURACO_CLIENT_SECRET"],
"invoices:read", tenant=os.environ.get("TAURACO_TENANT"))["access_token"]
for invoice in iterate("/invoices", token, {"status": "PAID", "limit": "100"}):
# Montants : chaînes décimales en unités majeures ("1180000" XOF), jamais des flottants.
print(invoice["number"], invoice.get("customer_name"), invoice["total"], invoice["currency"])
if __name__ == "__main__":
main()
Authorization code et PKCE (accès délégué)
Pour une application qui agit au nom d'un utilisateur : un assistant IA connecté par MCP, une application tierce interactive.
- Le client redirige vers
/oauth/authorizeavecresponse_type=code,client_id,redirect_uri(égalité exacte avec une adresse déclarée),code_challengeetcode_challenge_method=S256,state,scopeetresource. - L'utilisateur se connecte à Tauraco, choisit l'entreprise parmi celles où il a un rôle actif, voit l'hôte de redirection en clair et accepte ou refuse les scopes.
- Le client reçoit
code,stateetiss(RFC 9207), puis échange le code (valable 5 minutes, à usage unique) contre un jeton d'accès de 60 minutes et un jeton de rafraîchissement de 30 jours. - Chaque rafraîchissement rend un nouveau jeton de rafraîchissement. Réutiliser un jeton de rafraîchissement déjà échangé révoque toute la famille de jetons et l'octroi.
Un accès délégué porte les droits de l'utilisateur dans l'entreprise choisie, jamais plus : si l'utilisateur perd son rôle, son compte ou l'accès à l'entreprise, l'application perd l'accès à l'appel suivant.
Jeton d'accès
Jeton JWT signé RS256 (RFC 9068), en-tête typ at+jwt, clé kid publiée dans le JWKS. Il contient iss, aud (tauraco-public-api ou tauraco-mcp), sub, client_id, tenant_id, grant_id, scope, jti, iat et exp ; jamais de rôle, d'adresse e-mail ni de nom. Traitez-le comme opaque : sa forme peut évoluer.
Un jeton n'est valable que pour son audience : un jeton de l'API publique est refusé par le serveur MCP, et inversement. Les jetons de session de l'application Tauraco ne sont jamais acceptés par l'API publique.
Révocation et cycle de vie
À chaque appel, Tauraco relit l'application (active), l'octroi (actif, non expiré), l'utilisateur porteur (actif, non supprimé) et l'entreprise (active). Une application suspendue, un octroi révoqué ou un utilisateur délégant sans rôle actif donnent 401 grant_revoked dès la requête suivante. La rotation d'un secret invalide l'ancien immédiatement.
Scopes
Les scopes d'écriture impliquent la lecture correspondante (invoices:write donne invoices:read, exports:write donne exports:read, bank:info-requests:write donne bank:requests:read, bank:reconciliation:write donne bank:repayments:read). Un scope est vérifié en plus des droits de l'application : il ne donne jamais plus que ce qu'un utilisateur de l'entreprise pourrait voir.
| Scope | Accès | Clients |
|---|---|---|
company:read | profil de l'entreprise, application appelante | partenaire, MCP |
customers:read | clients | partenaire, MCP |
customers:write | création de clients | partenaire |
suppliers:read | fournisseurs (sans coordonnées bancaires) | partenaire, MCP |
invoices:read | factures clients et leurs lignes | partenaire, MCP |
invoices:write | création de factures clients | partenaire |
bills:read | factures fournisseurs | partenaire, MCP |
accounting:read | plan de comptes, journaux, écritures, balance, grand livre | partenaire, MCP |
payments:read | encaissements | partenaire, MCP |
payments:write | enregistrement d'encaissements | partenaire |
treasury:read | mouvements de trésorerie | partenaire, MCP |
purchases:read | bons de commande | partenaire, MCP |
products:read | produits et services | partenaire, MCP |
inventory:read | articles, mouvements et soldes de stock | partenaire, MCP |
tenders:read | avis de marchés publics relayés | partenaire, MCP |
markets:read | réservé, aucune ressource en v1 | partenaire, MCP |
financing:read | demandes de financement de l'entreprise | partenaire, MCP |
documents:read | contenu des pièces, en plus du scope du parent | partenaire, MCP |
guarantees:read | réservé, aucune ressource en v1 | partenaire, MCP |
collateral:read | dépôts et sûretés Collateral | partenaire, MCP |
exports:read | exports et leurs fichiers | partenaire, MCP |
exports:write | demande d'export | partenaire, MCP |
webhooks:manage | gestion des endpoints de webhooks | partenaire |
mcp:read | connexion au serveur MCP (exigé par toute requête /mcp) | MCP |
bank:inbox:read | boîte de réception de l'établissement | banque |
bank:requests:read | dossiers de financement reçus | banque |
bank:documents:read | pièces des dossiers | banque |
bank:info-requests:write | demandes de compléments | banque |
bank:collateral:read | sûretés liées aux dossiers | banque |
bank:repayments:read | échéances et remboursements | banque |
bank:reconciliation:write | rapprochement des remboursements | banque |
Les clients « internes » de Tauraco reçoivent les mêmes scopes qu'un partenaire. Un client banque ne reçoit que des scopes bank:*.
Erreurs OAuth
Les points /oauth/* répondent au format RFC 6749 ({"error":"invalid_grant","error_description":"…"}) : invalid_request, invalid_client (401, avec WWW-Authenticate: Basic), invalid_grant, unauthorized_client, unsupported_grant_type, unsupported_response_type, invalid_scope, invalid_target (RFC 8707), access_denied (consentement refusé), temporarily_unavailable (débit du point de jeton ou service indisponible) ; l'enregistrement dynamique ajoute invalid_redirect_uri et invalid_client_metadata (RFC 7591). Les erreurs de l'API publique sont décrites dans Gestion des erreurs.
Bonnes pratiques
- Ne demandez que les scopes nécessaires ; l'administrateur les voit tous avant d'autoriser.
- Mettez le jeton en cache jusqu'à
expires_inmoins une marge ; ne demandez pas un jeton par requête (le point/oauth/tokena son propre débit). - Ne journalisez jamais un secret ni un jeton ; journalisez
client_idetX-Request-Id.