API - Authentification
API - Authentification
Cette page regroupe les étapes indispensables pour consommer l'API temps réel côté serveur : préparation de l'environnement, méthodes d'authentification et récupération des réglages des devices avant d'ouvrir des flux.
1. Préparer l'environnement
- Identifiez l'URL de base de l'API (
https://api.app.phyling.fr,http://192.168.1.42:5001, etc.). - Munissez-vous d'un client HTTP (curl, Postman, Insomnia…) et d'outils de développement capables de gérer Socket.IO lorsque vous passerez à la partie temps réel.
2. Authentification
2.1 Utilisation d'une clé API
Important : une clé API est liée à un utilisateur précis et effectue toutes les requêtes en son nom. Créez un utilisateur manager dédié si vous devez couvrir tous les devices d'un compte client.
- Activation : contactez Phyling à contact@phyling.fr pour activer les clés API sur votre compte.
- Génération : connectez-vous sur
app.phyling.fr, ouvrez les paramètres avancés, puis la section Clés API et créez une nouvelle clé. - Utilisation : ajoutez l'en-tête suivant à vos requêtes REST.
Authorization: ApiKey <votre-clé>Exemple de récupération des réglages device :
GET /devices/rt/{client_id}/{device_number}/settings
Authorization: ApiKey abc123def456...Cette approche supprime la nécessité de vous connecter ou de gérer des tokens JWT.
Exemple minimal
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Phyling – minimal</title>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
</head>
<body>
<main>
<h1>Phyling – minimal</h1>
<pre id="log" style="height: 90vh; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;"></pre>
</main>
<script>
async function main() {
const $ = (sel) => document.querySelector(sel);
const logBox = $('#log'); // to show logs
const apiUrl = 'https://api.app.phyling.fr';
const apiKey = '<key>';
const clientId = 2;
const deviceNumber = 10300378;
const authorization = `ApiKey ${apiKey}`
const socketBase = apiUrl.replace(/^http/, 'ws');
const socket = io(socketBase, { transports: ['websocket'] });
socket.on('connect', () => {
socket.emit('subscribe', { authorization, room: `app/client/${clientId}/device/list_connected` });
socket.emit('subscribe', { authorization, room: `app/device/${deviceNumber}/ind/json/all` });
socket.emit('subscribe', { authorization, room: `app/device/${deviceNumber}/board/status` });
socket.emit('subscribe', { authorization, room: `app/device/${deviceNumber}/data/json/all` });
});
socket.onAny((event, ...args) => {
logBox.textContent += `⟵ event: ${event}\n${JSON.stringify(args[0], null, 2)}\n`;
});
}
main();
</script>
</body>
</html>2.2 Connexion via OAuth 2.0
Pour une application tierce, OAuth 2.0 permet de déléguer l'authentification sans exposer les identifiants utilisateur. Une application doit être créée côté Phyling (client_id, client_secret, URL de redirection). Contactez contact@phyling.fr pour activer la création d'applications OAuth.
- Rediriger l'utilisateur vers l'autorisation :
GET /oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&state={state}Exemple :
https://api.app.phyling.fr/oauth/authorize?client_id=app_123&redirect_uri=https://monapp.com/callback&state=xyz789- Récupérer le code d'autorisation : après validation, l'utilisateur est renvoyé vers votre
redirect_uri.
https://monapp.com/callback?code=abc123def456&state=xyz789- Échanger le code contre des tokens :
POST /oauth/token
Content-Type: application/json
{
"client_id": "app_123",
"client_secret": "secret_456",
"grant_type": "authorization_code",
"code": "abc123def456",
"redirect_uri": "https://monapp.com/callback"
}/oauth/token accepte aussi la forme standard OAuth 2.0 (RFC 6749) : un corps application/x-www-form-urlencoded et les identifiants de l’application dans un en-tête Authorization: Basic base64(client_id:client_secret). Les bibliothèques OAuth2 courantes (authlib, requests-oauthlib, Spring Security…) fonctionnent donc sans configuration particulière :
POST /oauth/token
Authorization: Basic YXBwXzEyMzpzZWNyZXRfNDU2
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=abc123def456&redirect_uri=https%3A%2F%2Fmonapp.com%2FcallbackSi le client_id est envoyé à la fois dans l’en-tête Basic et dans le corps, les deux valeurs doivent être identiques, sans quoi la requête est refusée.
Réponse type :
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"token_type": "Bearer",
"user": {
"client_id": 1234,
"mail": "user@example.com",
"...": "..."
}
}- Renouveler via
refresh_token:
POST /oauth/token
Content-Type: application/json
{
"client_id": "app_123",
"client_secret": "secret_456",
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOi...",
"redirect_uri": "https://monapp.com/callback"
}Au grant refresh_token, redirect_uri est facultatif — il n’est plus exigé.
Chaque échange remplace le refresh token : conservez celui renvoyé dans la réponse et jetez le précédent. Tant que votre application appelle /oauth/token au moins une fois par mois, l’utilisateur n’a jamais à se reconnecter.
Si la réponse se perd en route, rejouer le même refresh token dans les 30 secondes renvoie exactement le couple déjà émis — l’appel est donc sûr à retenter. Passé ce délai, présenter un refresh token déjà échangé est traité comme une compromission : toute la session est révoquée et l’utilisateur doit réautoriser l’application. Ne conservez donc jamais deux refresh tokens en parallèle pour une même session.
Utilisez ensuite access_token dans l'en-tête Authorization: Bearer <token> pour toutes les routes REST et temps réel.
- Connaître l’utilisateur connecté :
GET /oauth/userinfo
Authorization: Bearer <access_token>{
"sub": "42",
"user_id": 42,
"email": "user@example.com",
"firstname": "Jean",
"lastname": "Dupont",
"active": true
}user_id est l’identifiant Phyling de l’utilisateur, stable dans le temps : c’est lui qu’il faut stocker de votre côté. Cette route n’accepte qu’un access token OAuth2 (ni clé API, ni token du login web). Le reste des informations se lit sur les routes habituelles de l’API, avec le même token.
- Révoquer un token (RFC 7009) :
POST /oauth/revoke
Authorization: Basic YXBwXzEyMzpzZWNyZXRfNDU2
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOi...&token_type_hint=refresh_tokenLa session derrière le token est coupée : aucun nouveau token ne pourra en être obtenu. La réponse est toujours 200, même si le token est inconnu, expiré ou déjà révoqué. Les access tokens déjà émis restent valables jusqu’à leur expiration (1 h au maximum).
- Déconnecter l’utilisateur courant :
POST /oauth/logout
Authorization: Bearer <access_token>{
"revoked_sessions": 2
}À utiliser pour un bouton « Se déconnecter » dans votre interface. Toutes les sessions que cet utilisateur a ouvertes sur votre application sont révoquées, sur tous ses appareils. Vos autres utilisateurs ne sont pas affectés.
Exemple minimal
#!/usr/bin/env python3
"""
Minimal OAuth + realtime example for Phyling.
Installation et exécution :
> pip install -r requirements.txt
> python minimal_oauth.py --api-url https://api.app.phyling.fr --port 2000 --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET --device-number 10300042
Then open http://localhost:2000/ in your browser.
""" # noqa: E501
import argparse
import secrets
import sys
from typing import Any
from typing import Dict
from urllib.parse import urlencode
import requests
from flask import Flask
from flask import redirect
from flask import render_template_string
from flask import request
from flask import session
from flask import url_for
LOGIN_TEMPLATE = """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Connexion Phyling</title>
<style>
body { font-family: sans-serif; margin: 40px; }
main { max-width: 420px; }
button { padding: 10px 18px; font-size: 16px; cursor: pointer; }
</style>
</head>
<body>
<main>
<h1>Démo OAuth Phyling</h1>
<p>Ce mini serveur expose uniquement <code>/</code> : on y affiche ce bouton, puis on revient ici après OAuth.</p>
<button onclick="window.location.href='{{ authorize_url }}'">Se connecter avec Phyling</button>
<p style="margin-top:2rem;font-size:13px;color:#666;">Redirigé vers : <code>{{ redirect_uri }}</code></p>
</main>
</body>
</html>
"""
ERROR_TEMPLATE = """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Erreur OAuth</title>
</head>
<body style="font-family: sans-serif; margin: 40px;">
<h1>Impossible de poursuivre</h1>
<pre>{{ message }}</pre>
<a href="/">Revenir à l'écran de connexion</a>
</body>
</html>
"""
MINIMAL_TEMPLATE = """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Phyling – OAuth + temps réel</title>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
</head>
<body>
<main>
<h1>Phyling – temps réel (OAuth)</h1>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<span style="color: #0a0;">✅ Connecté</span>
<button id="logout-button" style="padding: 8px 14px; cursor: pointer;">Se déconnecter</button>
</div>
<pre id="log" style="height: 80vh; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;"></pre>
</main>
<script>
async function main() {
const $ = (sel) => document.querySelector(sel);
const logBox = $('#log');
const logoutBtn = $('#logout-button');
const apiBase = {{ api_url|tojson }};
const accessToken = {{ access_token|tojson }};
const clientId = {{ realtime_client_id|tojson }};
const deviceNumber = {{ device_number|tojson }};
const logoutUrl = {{ logout_url|tojson }};
const authorization = `Bearer ${accessToken}`;
const socketBase = apiBase.replace(/^http/, 'ws');
const socket = io(socketBase, { transports: ['websocket'] });
logoutBtn?.addEventListener('click', async () => {
try {
await fetch(logoutUrl, { method: 'POST', credentials: 'same-origin' });
} finally {
window.location.href = '/';
}
});
const log = (msg, payload) => {
logBox.textContent += `${msg}\\n${JSON.stringify(payload, null, 2)}\\n`;
logBox.scrollTop = logBox.scrollHeight;
};
socket.on('connect', () => {
log('✅ Connecté au socket', { socketId: socket.id });
socket.emit('subscribe', { authorization, room: `app/client/${clientId}/device/list_connected` });
socket.emit('subscribe', { authorization, room: `app/device/${deviceNumber}/ind/json/all` });
socket.emit('subscribe', { authorization, room: `app/device/${deviceNumber}/board/status` });
socket.emit('subscribe', { authorization, room: `app/device/${deviceNumber}/data/json/all` });
});
socket.onAny((event, ...args) => log(`⟵ event: ${event}`, args[0]));
socket.on('connect_error', (err) => log('❌ Socket error', err.message || err));
}
main();
</script>
</body>
</html>
"""
def create_app(config: Dict[str, Any]) -> Flask:
app = Flask(__name__)
app.secret_key = "secretkeyforflasksession"
app.config["DEMO"] = config
app.config["OAUTH_STATES"] = set()
@app.route("/", methods=["GET"])
def index():
cfg = app.config["DEMO"]
if "oauth_session" in session:
auth = session["oauth_session"]
return render_template_string(
MINIMAL_TEMPLATE,
api_url=cfg["api_url"],
access_token=auth["access_token"],
realtime_client_id=auth["realtime_client_id"],
device_number=cfg["device_number"],
logout_url=url_for("logout"),
)
pending_states = app.config["OAUTH_STATES"]
state = secrets.token_urlsafe(16)
pending_states.add(state)
authorize_query = urlencode(
{
"client_id": cfg["oauth_client_id"],
"redirect_uri": cfg["redirect_uri"],
"state": state,
}
)
authorize_url = f"{cfg['api_url']}/oauth/authorize?{authorize_query}"
return render_template_string(
LOGIN_TEMPLATE,
authorize_url=authorize_url,
redirect_uri=cfg["redirect_uri"],
)
@app.route("/callback", methods=["GET"])
def oauth_callback():
cfg = app.config["DEMO"]
pending_states = app.config["OAUTH_STATES"]
code = request.args.get("code")
incoming_state = request.args.get("state")
if not code:
return (
render_template_string(
ERROR_TEMPLATE, message="Missing authorization code"
),
400,
)
if not incoming_state or incoming_state not in pending_states:
return (
render_template_string(
ERROR_TEMPLATE, message="Invalid or missing state"
),
400,
)
pending_states.discard(incoming_state)
token_url = f"{cfg['api_url']}/oauth/token"
try:
token_resp = requests.post(
token_url,
json={
"client_id": cfg["oauth_client_id"],
"client_secret": cfg["client_secret"],
"grant_type": "authorization_code",
"code": code,
"redirect_uri": cfg["redirect_uri"],
},
timeout=10,
)
token_resp.raise_for_status()
payload = token_resp.json()
except requests.RequestException as exc:
return (
render_template_string(
ERROR_TEMPLATE, message=f"Token request failed: {exc}"
),
502,
)
except ValueError:
return (
render_template_string(
ERROR_TEMPLATE, message="Token response is not JSON"
),
502,
)
access_token = payload.get("access_token")
if not access_token:
return (
render_template_string(
ERROR_TEMPLATE,
message=f"Missing access_token in response: {payload}",
),
502,
)
realtime_client_id = payload.get("user", {}).get("client_id") or cfg.get(
"fallback_client_id"
)
if realtime_client_id is None:
return (
render_template_string(
ERROR_TEMPLATE,
message="Unable to resolve realtime client_id (token.user.client_id missing)",
),
500,
)
session["oauth_session"] = {
"access_token": access_token,
"realtime_client_id": realtime_client_id,
}
return redirect(url_for("index"))
@app.route("/logout", methods=["POST"])
def logout():
session.pop("oauth_session", None)
return ("", 204)
return app
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Mini démonstration OAuth + Socket.IO Phyling"
)
parser.add_argument(
"--api-url",
required=True,
help="URL de base de l'API Phyling (ex: http://localhost:5001)",
)
parser.add_argument(
"--client-id", required=True, help="Client ID OAuth fourni par Phyling"
)
parser.add_argument(
"--client-secret", required=True, help="Client secret OAuth fourni par Phyling"
)
parser.add_argument(
"--port", type=int, default=2000, help="Port local d'écoute (callback sur /)"
)
parser.add_argument(
"--device-number",
type=int,
default=10300042,
dest="device_number",
help="Device number à écouter après connexion",
)
parser.add_argument(
"--fallback-client-id",
type=int,
default=None,
dest="fallback_client_id",
help="Client ID temps réel à utiliser si le token ne contient pas user.client_id",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
api_url = args.api_url.rstrip("/")
redirect_uri = f"http://localhost:{args.port}/callback"
config = {
"api_url": api_url,
"oauth_client_id": args.client_id,
"client_secret": args.client_secret,
"redirect_uri": redirect_uri,
"device_number": args.device_number,
"fallback_client_id": args.fallback_client_id,
}
app = create_app(config)
try:
app.run(host="127.0.0.1", port=args.port, debug=False)
except OSError as exc:
print(f"Impossible de démarrer le serveur: {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()