Elchi Docs

↑ ↓ to move · Enter to open · Esc to close

Organisations

For products sold to companies. The people who use your application belong to the companies that bought it, hold a role in each, and your application learns from the tokens which company a person signed in for and what they may do there.

Turning them on

In the console, open the application, and under Settings tick Enable organisations. The application's Organisations page lists them from then on. Every application starts with two roles, admin and member, with member as the default.

Until organisations are on, nobody is asked for one and the tokens carry none, so a consumer product never sees any of this. Turned off later, the sessions go on and their tokens stop carrying organisations; turned on again, the same sessions carry them again.

What there is

An organisation is one of your customers: a name, a slug (lower case letters, digits and hyphens, like acme-ag, and never in the form of an id) and an id that never changes.

A role belongs to your application: a key like billing-admin, a label for people, and permissions in your own words, like invoices:read. EAuth does not interpret permissions; it passes them on. One role is the default for new members.

A member is an account of your application in an organisation, with one role there and a status, active or suspended. One account can belong to several organisations, with a different role in each.

Getting people in

By invitation. On the organisation's page, under Invite, an address and a role. The invitation goes out by mail with a link that works for seven days. The link opens a page on EAuth that joins with a button, never on opening, because mail scanners open every link. Only the account with the invited address can accept; anybody without one creates it from the same page and comes straight back. The browser the link was opened in keeps the invitation for a day while that happens; another browser needs the link again.

An invitation goes out five times at most (each time, the earlier link stops working), and can be withdrawn. One address gets at most ten invitation mails from an application a day, however many of its organisations invite it and however often an invitation is withdrawn and made again.

An account that exists. Under Add an existing account, by address. The person is a member at once, without a mail.

Through the organisation's identity provider. With single sign-on, the provider vouches for its people: the account is created at their first sign-in, and the membership too if the organisation wants it.

By domain. Give the organisation its email domain and add the TXT record the console shows:

_eauth.acme.example  TXT  "eauth-verification=..."

Then Verify. From then on you can turn on joining by domain: anybody whose confirmed address is at that domain joins with the default role the next time they sign in. Only a confirmed address counts, since anybody can type an address in. A domain can be verified by one organisation per application, and changing it takes the verification and automatic joining away until the new one is verified.

At sign-in

Pass organization to https://eauth.me/authorize, with the organisation's id or its slug:

https://eauth.me/authorize?client_id=...&redirect_uri=...&response_type=code
  &scope=openid%20email%20offline_access&code_challenge=...
  &code_challenge_method=S256&state=...&organization=acme-ag

The person then signs in to that organisation, or your redirect URI gets error=access_denied: when they are not an active member of it, when it is suspended, and when there is no such organisation. The answer is the same for all three, so it does not tell anybody which organisations exist.

Without the parameter:

The person is in The sign-in
no organisation carries none; your application decides what such a person may do
one carries that one
several shows a page to choose, with only their organisations on it
several, with prompt=none carries the one they used last, since no page may be shown

To let somebody switch, send them to sign in again with the other organisation's slug as organization. They are signed in to EAuth already, so it is one redirect and back.

A suspended organisation, and a suspended membership, are as if they did not exist: not offered, and refused when named.

If the database cannot answer for a moment while a refresh token is being exchanged, /token answers 503 with temporarily_unavailable and a Retry-After, and the refresh token stays valid. Only a refresh token that is no longer allowed gets invalid_grant.

In the tokens

When the sign-in was for an organisation, the ID token, the access token and /userinfo carry:

Claim
org_id The organisation's id. Key on this; it never changes.
org_slug Its slug, which an admin can change.
org_name Its name, for display.
org_role The key of the person's role there.
org_permissions That role's permissions, sorted.
{
  "sub": "01a06188-ea2a-7c15-8ac2-d33d2eacd4db",
  "email": "anna@acme.example",
  "org_id": "01a0dc84-5949-770c-9c29-f3aa5828032f",
  "org_slug": "acme-ag",
  "org_name": "Acme AG",
  "org_role": "billing-admin",
  "org_permissions": ["invoices:pay", "invoices:read"]
}

Your API verifies the access token as usual (the signature against the JWKS, iss, aud, exp), then checks that org_id is the organisation the requested data belongs to, and that org_permissions holds what the request needs. Both checks belong on the server: a browser decides what is shown, not what can be read.

When something changes

A role, or its permissions. The next refresh carries the change. Access tokens live for 15 minutes, so that is the longest an old role lasts.

A member removed or suspended, an organisation suspended or deleted. The refresh tokens issued in that organisation stop at once, and your webhook hears session.revoked with the reason. A code issued for it and not yet exchanged stops too. Access tokens already issued run out within 15 minutes; a check against your own records closes that gap where it matters.

A member removed stays out. Joining by domain does not bring them back at their next sign-in, and an invitation still open for their address is withdrawn with the removal. Inviting them again, or adding them, does bring them back.

Two-factor authentication

An organisation can require it: under Settings, Members need two-factor authentication. A member without it who signs in to that organisation is shown a page that leads to setting it up and back to the sign-in; with prompt=none your application gets interaction_required. Refresh tokens of members without it stop at the next refresh. The application's own setting, Require two-factor for every user of this app, works the same way for every sign-in.

Two-factor here means an authenticator app on the account. With one set up, no password alone gets in: a sign-in needs the code, or a passkey that checked the person.

Webhooks

Type When data
organisation.created An organisation was created in the console. organisation_id, slug, name
organisation.deleted An organisation was deleted. organisation_id, slug
organisation.member_added Somebody joined: by invitation, added in the console, or by domain. organisation_id, user_id, role, via
organisation.member_updated A member's role or status changed. organisation_id, user_id, role, status
organisation.member_removed A member was removed. organisation_id, user_id

See webhooks for delivery, signatures and retries.

Leaving

Users, then Export everything, includes every organisation with its members and their roles, and the roles with their permissions, next to the accounts.

7 min read