Skip to main content
Version: Target · roadmap

Identity

Who is talking to the station? A room starts anonymous. It can then be identified by data the person gives, then authenticated when a provider proves that this data really is theirs. Permissions and policies read the result; they do not know how it was obtained.

Declared, verified​

An identity datum carries a name the station chooses — phone, email, customer_id… — and a value. It has one of two levels.

declared — the room is identifiedverified — the person is authenticated
comes froma form field marked identifies: true: the person typed ita provider that proved it: a code received, an SSO login, the tenant's login
provesnothing about the personthat they control this number, this e-mail, this account
holds forthis room onlythe person: the room belongs to them
givesthe datum is not asked again, and goes to the tenant with the submitin addition: their other rooms, their history, and submitted_by: owner
finds other roomsneveryes, for them alone

A typed number never finds anything: anyone can type someone else's number, and that must show neither their orders nor their address. Finding it from one room to another requires the same verified datum.

Identifying a room: identifies: true​

A form field identifies the room; the datum carries the name of the field.

skills:
- name: takeDeliveryOption
select:
guards:
- form:
phone: {type: phone, required: true, identifies: true}
address: {type: text}

The room keeps the number at the declared level: the room's next forms no longer ask for it — Core answers it —, and it goes out with the submit. The model knows the room has a phone, never which one. Two fields that declare the same name with different types are refused at load time.

Verifying: the providers​

The station declares the providers it accepts, in station.identity.providers. Each one is a mechanism that Convoy prepares: you configure it, you do not code it. Each one states the datum it verifies (verifies:).

mechanismprovesconfigured with
otpthe person receives the codes sent to this datum (a phone, an e-mail)verifies, then via: tenant (the tenant's send and check operations, in its apis) or via: convoy (the service Convoy operates, channel: sms or email)
oidcthe person signed in with an identity provider: Okta, Auth0, Keycloak, Entra ID, Google…verifies (the claim that names the person), issuer, client_id_env, scopes, attributes
assertionthe tenant's site has already signed the person inverifies (the tenant's customer identifier), secret_env, attributes, login_url (its sign-in page, for an authentication request)

The current version already has otp with via: tenant: see Auth.

attributes says which other fields of the provider the rules can read, and under which name: {roles: groups, teams: teams, manager: manager} for a directory.

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}}

At load time, a configuration that cannot work is refused: via: tenant with operations the tenant does not declare, an oidc without client_id_env, an auth: that names a missing provider.

Convoy never keeps a secret of the person: no password, no code, no login page of its own. It keeps the result — this datum is verified, by this provider, at this time.

Each portal says how it proves who is talking​

A portal names the provider it offers, or anonymous:

agent:
portals:
web:
widget: {identity: {auth: compte}} # on the tenant's site, its login is enough
pwa: {identity: {auth: sms}} # off the site, a code by SMS
teams: {app: commandes, identity: {auth: salaries}}

The portal is described in The agent and its portals. When a skill requires a proof, this is the provider Core offers: see Asking for authentication.

What the rules read: identity​

Profiles, permissions and policies read an identity object. It gives names and attributes, never the values of identity data: a rule tests that a phone is verified, not which number it is.

fieldmeaningcomes from
identity.typeperson or agentthe door
identity.authenticatedat least one verified datumthe providers
identity.verifiedthe names of the verified data: 'phone' in identity.verifiedthe providers
identity.declaredthe names of the data declared in this roomthe forms
identity.providerthe provider that verifiedthe providers
identity.<attribute>roles, teams, manager, addresses…the provider's attributes
identity.org, identity.station, identity.roles, identity.limitsfor an agent: its organisation, the calling station, the roles and limits set on its sub-identitythe delegated sub-identity
profiles:
anonyme: {when: "identity.authenticated == false"}
client: {when: "'phone' in identity.verified"}
manager: {when: "'support-manager' in identity.roles"}

The room and its owner​

When a datum is verified, the room is claimed: it now belongs to this person, its owner (owner). Their other rooms are given back to them, to them alone.

submitted_by: [{who: owner}] requires this verified owner at submit. Without this rule, a human in the room submits, even anonymous: the customer's click. With it, holding the room's code is no longer enough — you must receive the code, or be signed in. See The submit.

Recommended for every station that places orders or requests with the merchant. Without a sign-in, nothing tells the customer on another device from a stranger holding the code: both can finalise. Where the station can sign people in but does not require submitted_by: owner, the selection invites the customer to sign in before finalising, without making them.

Identity data, protected​

  • The value is kept because the tenant needs it in its submit, and for nothing else.
  • Lookups go through a fingerprint (HMAC, one key per tenant, the tenant included in the fingerprint): no index holds a number in clear. The provisioner derives each tenant's key from the deployment's; changing that one signs everyone out.
  • It follows the room's lifetime; erasure requested by the owner erases it.

Three examples​

Simple — the room identified by a number, without proof. The tenant receives the number at submit, the room remembers it; submits stay anonymous.

skills:
- name: takeDeliveryOption
select:
guards:
- form:
phone: {type: phone, required: true, identifies: true}

Medium — the number verified by a code, and only the owner submits.

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}]

Advanced — one path per portal. The customer signed in on the site is verified without typing anything; off the site, they receive a code; an employee goes through the company's SSO, which gives their roles.

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}]