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.