Identité
Qui parle à la station ? Une room commence anonyme. Elle peut ensuite être identifiée par une donnée que la personne donne, puis authentifiée quand un fournisseur prouve que cette donnée est bien la sienne. Les permissions et les politiques lisent le résultat ; elles ne savent pas comment il a été obtenu.
Déclaré, vérifié
Une donnée d'identité porte un nom que la station choisit — phone, email, customer_id… —
et une valeur. Elle a l'un de deux niveaux.
| déclaré — la room est identifiée | vérifié — la personne est authentifiée | |
|---|---|---|
| vient de | un champ de formulaire marqué identifies: true : la personne l'a tapé | un fournisseur qui l'a prouvé : un code reçu, une connexion SSO, le login du tenant |
| prouve | rien sur la personne | qu'elle contrôle ce numéro, cet e-mail, ce compte |
| vaut pour | cette room seulement | la personne : la room lui appartient |
| donne | la donnée n'est plus redemandée, et part au tenant avec le submit | en plus : ses autres rooms, son historique, et submitted_by: owner |
| retrouve d'autres rooms | jamais | oui, pour elle seule |
Un numéro tapé ne retrouve jamais rien : n'importe qui peut taper le numéro d'un autre, et cela ne doit montrer ni ses commandes ni son adresse. Le retrouver d'une room à l'autre demande la même donnée vérifiée.
Identifier une room : identifies: true
Un champ de formulaire identifie la room ; la donnée porte le nom du champ.
skills:
- name: takeDeliveryOption
select:
guards:
- form:
phone: {type: phone, required: true, identifies: true}
address: {type: text}
La room garde le numéro au niveau déclaré : les formulaires suivants de la room ne le demandent plus — Core y répond —, et il part au submit. Le modèle sait que la room a un téléphone, jamais lequel. Deux champs qui déclarent le même nom avec des types différents sont refusés au chargement.
Vérifier : les fournisseurs
La station déclare les fournisseurs qu'elle accepte, dans station.identity.providers. Chacun est
un mécanisme que Convoy prépare : on le configure, on ne le code pas. Chacun dit la donnée
qu'il vérifie (verifies:).
| mécanisme | prouve | se configure avec |
|---|---|---|
otp | la personne reçoit les codes envoyés à cette donnée (un téléphone, un e-mail) | verifies, puis via: tenant (les opérations send et check du tenant, dans ses apis) ou via: convoy (le service que Convoy opère, channel: sms ou email) |
oidc | la personne s'est connectée chez un fournisseur d'identité : Okta, Auth0, Keycloak, Entra ID, Google… | verifies (le claim qui nomme la personne), issuer, client_id_env, scopes, attributes |
assertion | le site du tenant a déjà connecté la personne | verifies (l'identifiant client du tenant), secret_env, attributes, login_url (sa page de connexion, pour une demande d'authentification) |
La version actuelle a déjà otp avec via: tenant : voir Auth.
attributes dit quels autres champs du fournisseur les règles peuvent lire, et sous quel nom :
{roles: groups, teams: teams, manager: manager} pour un annuaire.
station:
identity:
providers:
sms: {mechanism: otp, verifies: phone, via: tenant, send: sendOtp, check: checkOtp}
salaries: {mechanism: oidc, verifies: email, issuer: "https://acme.okta.com",
client_id_env: OKTA_CLIENT_ID, attributes: {roles: groups, teams: teams, manager: manager}}
compte: {mechanism: assertion, verifies: customer_id, secret_env: SHOP_IDENTITY_SECRET,
attributes: {addresses: addresses}}
Au chargement, une configuration qui ne peut pas marcher est refusée : via: tenant avec des
opérations que le tenant ne déclare pas, un oidc sans client_id_env, un auth: qui nomme un
fournisseur absent.
Convoy ne garde jamais un secret de la personne : pas de mot de passe, pas de code, pas de page de connexion à lui. Il garde le résultat — telle donnée est vérifiée, par tel fournisseur, à telle heure.
Chaque portail dit comment il prouve qui parle
Un portail nomme le fournisseur qu'il propose, ou anonymous :
agent:
portals:
web:
widget: {identity: {auth: compte}} # sur le site du tenant, sa connexion suffit
pwa: {identity: {auth: sms}} # hors du site, un code par SMS
teams: {app: commandes, identity: {auth: salaries}}
Le portail est décrit dans L'agent et ses portails. Quand une skill exige une preuve, c'est ce fournisseur que Core propose : voir Demander l'authentification.
Ce que les règles lisent : identity
Les profils, les permissions et les politiques lisent un objet identity. Il donne des noms et
des attributs, jamais les valeurs des données d'identité : une règle teste qu'un téléphone est
vérifié, pas quel numéro c'est.
| champ | sens | vient de |
|---|---|---|
identity.type | person ou agent | la porte |
identity.authenticated | au moins une donnée vérifiée | les fournisseurs |
identity.verified | les noms des données vérifiées : 'phone' in identity.verified | les fournisseurs |
identity.declared | les noms des données déclarées dans cette room | les formulaires |
identity.provider | le fournisseur qui a vérifié | les fournisseurs |
identity.<attribut> | roles, teams, manager, addresses… | les attributes du fournisseur |
identity.org, identity.station, identity.roles, identity.limits | pour un agent : son organisation, la station qui appelle, les rôles et les limites posés sur sa sous-identité | la sous-identité déléguée |
profiles:
anonyme: {when: "identity.authenticated == false"}
client: {when: "'phone' in identity.verified"}
manager: {when: "'support-manager' in identity.roles"}
La room et son propriétaire
Quand une donnée est vérifiée, la room est réclamée : elle appartient désormais à cette
personne, son propriétaire (owner). Ses autres rooms lui sont rendues, à elle seule.
submitted_by: [{who: owner}] exige ce propriétaire vérifié au submit. Sans cette règle, un humain
dans la room soumet, même anonyme : le clic du client. Avec elle, détenir le code de la room ne
suffit plus — il faut recevoir le code, ou être connecté. Voir Le submit.
Recommandé pour toute station qui passe des commandes ou des demandes chez le marchand. Sans
connexion, rien ne distingue la cliente sur un autre appareil d'un inconnu qui tient le code : les
deux peuvent finaliser. Si la station sait connecter mais n'exige pas submitted_by: owner, la
sélection invite le client à se connecter avant de finaliser, sans l'y obliger.
Les données d'identité, protégées
- La valeur est gardée parce que le tenant en a besoin dans son submit, et pour rien d'autre.
- Les recherches passent par une empreinte (HMAC, une clé par tenant, le tenant compris dans l'empreinte) : aucun index ne contient un numéro en clair. Le provisioner dérive la clé de chaque tenant de celle du déploiement ; changer celle-ci déconnecte tout le monde.
- Elle suit la durée de vie de la room ; l'effacement demandé par le propriétaire l'efface.
Trois exemples
Simple — la room identifiée par un numéro, sans preuve. Le tenant reçoit le numéro au submit, la room s'en souvient ; les submits restent anonymes.
skills:
- name: takeDeliveryOption
select:
guards:
- form:
phone: {type: phone, required: true, identifies: true}
Moyen — le numéro vérifié par un code, et seul le propriétaire soumet.
station:
identity:
providers:
sms: {mechanism: otp, verifies: phone, via: tenant, send: sendOtp, check: checkOtp}
skills:
- name: takeDeliveryOption
select:
guards:
- form:
phone: {type: phone, required: true, identifies: true}
agent:
portals:
web:
widget: {identity: {auth: sms}}
playbooks:
- name: commande
submit_guard:
submitted_by: [{who: owner}]
Avancé — une voie par portail. Le client connecté sur le site est vérifié sans rien taper ; hors du site, il reçoit un code ; un salarié passe par le SSO de l'entreprise, qui donne ses rôles.
station:
identity:
providers:
sms: {mechanism: otp, verifies: phone, via: convoy, channel: sms}
compte: {mechanism: assertion, verifies: customer_id, secret_env: SHOP_IDENTITY_SECRET}
salaries: {mechanism: oidc, verifies: email, issuer: "https://login.microsoftonline.com/acme/v2.0",
client_id_env: ENTRA_CLIENT_ID, attributes: {roles: groups}}
agent:
portals:
web:
widget: {identity: {auth: compte}}
pwa: {identity: {auth: sms}}
teams: {app: commandes-internes, identity: {auth: salaries}}
profiles:
acheteur: {when: "'purchasing' in identity.roles"}
playbooks:
- name: commande
submit_guard:
submitted_by: [{who: owner}]