Skip to main content
Comment cside détecte le partage de compte
Language

Détecter le partage de compte

Empêchez le partage de compte non autorisé et faites respecter vos sièges de licence. Taguez votre télémétrie avec le compte connecté, puis lisez la règle account_shared_detected depuis votre backend pour savoir quand un compte est utilisé depuis plus d'appareils que vous ne l'autorisez.

Un seul jeu d’identifiants, utilisé par plus de personnes que n’en ont payé l’accès : l’abonnement partagé, les deux sièges qui font tourner huit personnes en silence, le compte revendu. Rien de tout cela ne paraît anormal dans vos propres logs, puisque chaque connexion est une authentification réussie avec le bon mot de passe.

Device Intelligence ajoute ce qui manque aux logs : une identité d’appareil sur chaque session. Taguez votre télémétrie avec le compte qui se connecte, et le verdict arrive à votre backend au moment même de la connexion. Dans le tableau de bord, vous obtenez aussi la forme du problème : quels appareils, en provenance d’où, si les sessions se sont chevauchées, et si le compte a changé de région plus vite qu’il n’est humainement possible de voyager.

Le même mécanisme permet de faire respecter les sièges de licence. Réglez la limite sur le nombre de sièges vendus, et la règle vous indique quand un client les a discrètement dépassés.

Comment cside détecte le partage de compte

Taguez votre télémétrie avec l’id de compte de la personne connectée. cside compte les appareils distincts observés pour ce compte sur une fenêtre glissante, et lorsque ce nombre dépasse la limite de votre site, la règle se déclenche.

Cela se joue en deux moitiés, qui s’exécutent à des endroits différents :

  • Votre navigateur appelle sendClientTelemetry({ userId }) et reçoit un token en retour.
  • Votre serveur envoie ce token à cside et lit le verdict.

Gardez la vérification du verdict côté serveur. Votre clé API Device Intelligence ne doit jamais approcher le code navigateur, et un verdict que le navigateur pourrait lire est un verdict qu’il pourrait falsifier.

Ce dont vous aurez besoin

  • Device Intelligence activé sur le domaine
  • Le script installé et sendClientTelemetry appelé : consultez Configurer Device Intelligence
  • Une clé API Device Intelligence backend : consultez Events API
  • La règle activée pour votre domaine. L’activation en libre-service n’existe pas encore, demandez donc à cside de l’activer

L'écran de configuration de Device Intelligence avec l'onglet npm sélectionné, montrant la commande d'installation et l'appel initDeviceIntelligence L'onglet des paramètres de Device Intelligence montrant une clé API backend active, avec un rappel de garder les clés côté serveur

Dans le navigateur : envoyez l’id de compte

Taguer la télémétrie avec userId

Appelez sendClientTelemetry une première fois au chargement de la page sans userId, pour que la collecte démarre tôt, puis rappelez-la avec userId dès que vous savez qui est connecté. Transmettez directement le token renvoyé à votre propre backend ; le navigateur n’en fait rien d’autre.

Vous pouvez la rappeler à tout moment pour obtenir une lecture fraîche, pas seulement à la connexion. Chaque appel produit une nouvelle évaluation, donc pour les sessions longues, un appel périodique garde le nombre d’appareils à jour au lieu de vous laisser avec un verdict datant de la dernière connexion de la personne.

import { initDeviceIntelligence } from "@cside.dev/device-intelligence";

const { sendClientTelemetry } = await initDeviceIntelligence({
  teamID: "[your-team-id]",
});

// Call this at sign-in, and anywhere else you want to refresh the reading:
// long-running sessions benefit from a periodic call.
async function onSignIn(userId) {
  const { token, errors } = await sendClientTelemetry({ userId });

  if (errors) {
    console.error(errors);
    return;
  }

  // Your own backend route, not a cside endpoint. It does the actual
  // verdict check server-side; see the next section.
  await fetch("/api/account-sharing-check", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ token }),
  });
}
La clé doit correspondre à celle que lit la règle

userId est la valeur par défaut, pas une obligation. Ce qui compte, c’est que la clé que vous envoyez et celle que lit la règle soient identiques, orthographe comprise. Envoyer accountId ou user_id alors que la règle est toujours réglée sur userId n’est pas une erreur : la valeur est lue comme définitivement absente, et vous verrez outcome: 3 à chaque évaluation au lieu de outcome: 1 ou outcome: 2. Si vous envoyez déjà une clé différente, cside peut pointer la règle dessus à la place. Consultez Vous envoyez déjà un identifiant différent ?.

Aucune PII ni aucun secret dans les valeurs d'id externe

userId et tout autre id externe sont stockés et inclus dans les exports. Envoyez un identifiant de compte opaque, pas une adresse e-mail, un nom, ou toute autre donnée que vous ne voudriez pas voir persister.

Vous envoyez déjà un identifiant différent ?

Vous n’êtes pas obligé de passer à userId. Ce qui compte, c’est que la règle lise la même clé que celle que vous envoyez, et cside peut rebrancher la règle sur cette clé, donc rien ne change côté client.

La clé doit en revanche être l’une de userId, accountId, organizationId ou teamId. Ce sont les quatre seules sur lesquelles la règle peut s’appuyer, car ce mécanisme a besoin d’un champ dont l’ensemble de valeurs est borné. email, orderId et tout le reste peuvent toujours être envoyés et stockés, ils ne peuvent simplement pas servir de clé ; envoyez donc l’un des quatre en plus de ce que vous utilisez déjà.

Sur votre serveur : lisez le verdict

C’est la route /api/account-sharing-check de l’étape précédente. Elle échange le token contre l’évaluation complète et décide de la marche à suivre, entièrement côté serveur, avec votre clé API Device Intelligence.

curl https://api.cside.com/token/v2/verify \
  --request POST \
  --header "Authorization: Bearer $CSIDE_FINGERPRINT_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"token":"'"$CSIDE_FINGERPRINT_SESSION_TOKEN"'"}'

L’endpoint renvoie 202 Accepted avec { "status": "pending" } tant que l’évaluation est en cours, puis 200 OK une fois terminée. Interrogez-le jusqu’à environ 3 fois, à peu près toutes les 3 secondes comme borne haute prudente : le moteur de règles s’exécute de façon synchrone lors de l’enregistrement du résultat, donc le verdict est en général prêt dès la première tentative. Et il est prêt dans son intégralité : rule_evaluations_v2 est renseigné exactement au moment où status passe à "completed", jamais plus tard, donc dès que vous voyez "completed", une entrée absente est réellement absente, pas simplement en cours d’arrivée. Si vous épuisez vos tentatives et que le statut reste en attente, considérez qu’il n’y a pas de verdict pour cette connexion plutôt que de bloquer la personne : la prochaine connexion produira une évaluation fraîche.

Voici le tout assemblé : échangez le token, vérifiez qu’il appartient bien au compte réellement connecté, puis trouvez l’entrée de partage de compte et branchez votre logique dessus. Vérifiez à la fois outcome et evaluated_as dans le code lui-même, pas seulement le slug de la règle : d’autres règles, y compris l’expérience shadow de quelqu’un d’autre, peuvent voyager dans le même tableau.

// apps/api-style illustrative route handler. Adjust to your own framework.
// The wire sends these as integers. Naming them keeps the check readable;
// the full set of values is in the tables below.
const OUTCOME_TRUE = 1;
const LIFECYCLE_ACTIVE = 3;

app.post("/api/account-sharing-check", async (req, res) => {
  const { token } = req.body;
  const evaluation = await verifyToken(token);

  // Trust your own session, never the browser. The userId on the telemetry is
  // whatever the page chose to send, so bind it to the signed-in account
  // before you act on anything.
  if (evaluation.fingerprint?.external_ids?.userId !== req.session.userId) {
    return res.sendStatus(409);
  }

  const sharing = findAccountSharing(evaluation);

  if (sharing !== undefined) {
    // This account is over your site's device limit right now.
    // Step up auth, cap sessions, flag for review: see "Decide what to do" below.
  }

  res.sendStatus(204);
});

async function verifyToken(token, attempt = 0) {
  const response = await fetch("https://api.cside.com/token/v2/verify", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CSIDE_FINGERPRINT_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ token }),
  });

  if (!response.ok) {
    // An expired key, a bad token, a rate limit. Never fall through: a failed
    // request is an outage, not a verdict of "nobody is sharing this account".
    throw new Error(`Device Intelligence verify failed: ${response.status}`);
  }

  const data = await response.json();

  if (data.status === "pending" && attempt < 3) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    return verifyToken(token, attempt + 1);
  }

  if (data.status === "failed") {
    // Evaluation itself failed. Don't treat this the same as "no entry":
    // a failure isn't the healthy case.
    throw new Error("Device Intelligence evaluation failed");
  }

  return data;
}

function findAccountSharing(data) {
  // An absent entry means the rule wasn't evaluated, not that it
  // evaluated to false.
  return data.rule_evaluations_v2?.values?.find(
    (v) =>
      v.evaluation.rule_slug === "account_shared_detected" &&
      v.evaluation.outcome === OUTCOME_TRUE &&
      // Shadow rules ride in the same array. Acting on one would enforce
      // an experiment that isn't live for your site.
      v.evaluation.evaluated_as === LIFECYCLE_ACTIVE,
  );
}
Liez le token à votre propre session

Le userId d’un token est simplement ce que le navigateur a envoyé. N’importe qui peut l’omettre, ou y placer un autre compte, donc le traiter comme une preuve d’identité permet de pousser des observations sur un compte qui n’est pas le sien, ou de faire paraître son propre compte comme sain. Comparez fingerprint.external_ids.userId au compte de votre session serveur et refusez tout ce qui ne correspond pas. cside vous dit ce que le navigateur a déclaré ; vous seul savez qui est réellement connecté.

outcome et evaluated_as sont des entiers, pas des chaînes. Chaque règle évaluée et publiée rapporte une entrée à chaque événement, quel que soit le résultat obtenu : 1, 2 et 3 sont des états stables que vous verrez en continu, pas des incidents ponctuels. 4 est l’exception, et il signifie quelque chose de plus précis qu’il n’y paraît : la règle a besoin d’une mesure qui existe mais qui n’est pas encore prête. C’est différent d’un status à "pending", qui signifie que l’évaluation elle-même n’est pas terminée, ce à quoi sert la boucle d’interrogation ci-dessus. Interroger le même token plus intensément ne transformera pas un 4 en verdict ; il se résout lors d’évaluations ultérieures, à mesure que les données s’accumulent pour ce compte.

Même énumération que la référence rule_evaluations_v2 ; voici ce que signifie chaque valeur pour cette règle en particulier :

outcomeSignificationQuand vous l’observerez
0Non spécifiéJamais produit
1VraiEn continu, tant que le compte dépasse votre limite
2FauxEn continu, l’état normal d’un compte sain qui ne dépasse pas la limite
3Absent (structurel)En continu, si la règle ne peut pas lire userId dans votre télémétrie, par exemple à cause d’une mauvaise clé
4En attente (transitoire)Lorsque la mesure de la règle n’est pas encore prête. Ce n’est pas la même chose qu’un status: "pending", et réinterroger ce même token ne le fera pas disparaître
evaluated_asSignification
1Brouillon
2Shadow : l’expérience de quelqu’un d’autre, peut apparaître à tout moment, n’agissez pas dessus
3Active : la règle que votre site a activée
4Archivée

Une remarque sur transition, utile si vous préférez agir une seule fois plutôt qu’à chaque requête : une fois que vous avez déjà vérifié outcome: 1, transition: 3 signifie que le compte vient de franchir le seuil lors de cette évaluation, et transition: 2 signifie qu’il le dépasse toujours depuis une évaluation antérieure.

Le compte qui a déclenché la règle se trouve dans fingerprint.external_ids, dans la même réponse, pas dans evaluation.key. key est un hash de ce sur quoi la règle s’est appuyée et ne porte jamais l’id de compte en clair. Notez aussi que key.key_hash est une chaîne JSON, pas un nombre, et que ce corps de réponse mélange deux formats d’horodatage : au niveau racine, created_at et updated_at sont des nombres en millisecondes, tandis que evaluated_at et event_time à l’intérieur de l’évaluation sont des objets { seconds, nanos }.

Exemple de réponse tronquée, capturée lors d’une évaluation réelle, les autres champs sont omis. Remarquez la deuxième entrée : d’autres règles voyagent réellement dans le même tableau, c’est pourquoi le code ci-dessus filtre sur le slug, outcome et evaluated_as ensemble, pas sur le slug seul.

{
  "status": "completed",
  "created_at": 1789731723590,
  "updated_at": 1789731723590,
  "fingerprint": {
    "device_id": "dev_3f8a21c9",
    "external_ids": {
      "userId": "acct_9f2a1c"
    }
  },
  "rule_evaluations_v2": {
    "values": [
      {
        "evaluation": {
          "rule_slug": "account_shared_detected",
          "outcome": 1,
          "evaluated_as": 3,
          "transition": 3,
          "key": { "key_hash": "16386830586737169487" },
          "event_time": { "seconds": 1789731723, "nanos": 590000000 },
          "evaluated_at": { "seconds": 1789731723, "nanos": 605000000 }
        }
      },
      {
        "evaluation": {
          "rule_slug": "multi_account_detected",
          "outcome": 2,
          "evaluated_as": 3,
          "transition": 2,
          "key": { "key_hash": "261153104274697293" },
          "event_time": { "seconds": 1789731723, "nanos": 590000000 },
          "evaluated_at": { "seconds": 1789731723, "nanos": 605000000 }
        }
      }
    ]
  }
}

Non montré ci-dessus : certains objets imbriqués de la réponse complète utilisent des noms de champs en camelCase plutôt qu’en snake_case, lorsqu’ils partagent un format d’échange avec le concepteur de règles du tableau de bord (à l’intérieur de explanation, et de key.components[].dimension). Si vous affichez votre propre réponse complète et que vous voyez un mélange des deux, c’est normal, pas un bug de votre intégration.

Consultez la référence complète des champs rule_evaluations_v2 pour le reste des champs.

Décidez de la marche à suivre

cside signale, il ne bloque pas. Ce que vous faites d’une règle déclenchée vous appartient : renforcer l’authentification, plafonner les sessions simultanées du compte, ou le signaler pour une revue manuelle.

Confirmez que ça fonctionne

Commencez par confirmer que l’id arrive bien : échangez un token et vérifiez que fingerprint.external_ids contient la valeur attendue sous userId.

Vérifiez ensuite la règle elle-même, avant que qui que ce soit ne partage quoi que ce soit. Comme chaque règle évaluée et publiée fait un rapport à chaque événement, vous n’avez pas besoin de déclencher un partage pour savoir si le pipeline fonctionne :

  • outcome: 2 sur l’entrée account_shared_detected est ce à quoi ressemble une intégration saine et correctement câblée : la règle tourne, lit la bonne clé, et personne ne dépasse la limite. C’est votre confirmation négative, et vous la verrez en continu, pas juste une fois.
  • outcome: 3 signifie que vous envoyez la mauvaise clé. Corrigez-le maintenant, avant de tester plus loin ; consultez Vous envoyez déjà un identifiant différent ?.
  • Aucune entrée pour account_shared_detected signifie que la règle n’est pas encore activée pour ce domaine, qu’il s’agit d’une règle de mesure seule qui ne publie pas, ou que l’évaluation n’a pas tourné pour cet événement. Demandez à cside.
fenêtre glissante de 2 heures0h1h2h3hAppareil 1Appareil 2Appareil 3dépasse la limite du siteAppareil 4Connexion plus tard, hors fenêtrene compte pas

Observez-le passer à vrai

Une fois que vous avez confirmé la base saine outcome: 2 ci-dessus, déclenchez la règle intentionnellement pour observer le flux complet, transition comprise :

  1. Connectez-vous à un compte de test depuis trois navigateurs ou profils d’appareil différents, en moins de 2 heures. Ce sont les valeurs par défaut livrées ; si la limite ou la fenêtre de votre site a été ajustée, utilisez la vôtre à la place
  2. Échangez un token de session après chaque connexion et surveillez account_shared_detected : il reste à outcome: 2 pour les deux premières, puis passe à outcome: 1 (avec transition: 3) dès que le troisième appareil fait dépasser la limite du site au compte
  3. Confirmez evaluated_as: 3 en parallèle, pour être sûr de lire la règle active et non une expérience shadow

Des fenêtres de navigation privée sur la même machine physique peuvent ne pas être lues comme des appareils distincts, utilisez donc des navigateurs ou des profils d’appareil séparés plutôt que plusieurs fenêtres privées dans un seul navigateur.

curl https://api.cside.com/token/v2/verify \
  --request POST \
  --header "Authorization: Bearer $CSIDE_FINGERPRINT_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"token":"'"$CSIDE_FINGERPRINT_SESSION_TOKEN"'"}'

# rule_evaluations_v2 carries the rule outcomes, account_shared_detected among them.
# If "status" is "pending", wait a few seconds and retry, up to about 3 times.

Une fois que vous l’avez confirmé via l’API, ouvrez cette session dans le tableau de bord cside : sa page de détail de session a un onglet Identity graph qui montre la même chose visuellement, un compte se ramifiant vers plusieurs nœuds d’appareil.

Une session regroupée par l'id externe userId, couvrant quatre sessions sur quatre appareils Le graphe d'identité d'un compte, se ramifiant à travers quatre navigateurs vers quatre appareils et quatre réseaux, avec la règle de partage de compte déclenchée

Si votre équipe a accès à la page de cas d’usage partage de compte dans le tableau de bord, sa tuile de couverture est le moyen le plus direct de repérer une mauvaise clé : elle mesure la fraction de vos événements qui porte réellement l’id lu par la règle. Cette page est encore en accès anticipé, il se peut donc qu’elle ne soit pas activée pour vous.

L'écran Account sharing listant deux comptes détectés, avec l'un d'eux sélectionné montrant ses appareils groupés par navigateur

Étapes suivantes

Dépannage

Presque toutes les intégrations qui ne voient rien ont la même cause : l’id externe que vous envoyez n’est pas celui que lit la règle. userId est la valeur par défaut plutôt qu’une obligation, donc cside peut rebrancher la règle sur n’importe lequel des autres champs éligibles. Votre télémétrie et la règle doivent simplement s’accorder sur un seul.

  1. Vérifiez ce qui est arrivé. Échangez un token et regardez fingerprint.external_ids. Si votre id de compte n’y est pas, sendClientTelemetry a tourné sans lui, ou a tourné avant que vous sachiez qui se connectait.
  2. Vérifiez que la règle a pu le lire. Un outcome de 3 sur l’entrée account_shared_detected signifie que la règle a tourné et n’a pas trouvé sa clé.
  3. Vérifiez que la valeur est stable. Un id de session ou un token rotatif fait ressembler chaque connexion à un nouveau compte, si bien que rien ne s’accumule jamais pour un seul.
  4. Rebranchez plutôt que réécrire. Demandez à cside de pointer la règle vers votre clé à la place. Consultez Vous envoyez déjà un identifiant différent ?.
Was this page helpful?