<!-- https://docs.elchi.dev/organisations | EAuth | Elchi Docs -->

# 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](/enterprise-sso), 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. |

```json
{
  "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](https://eauth.me/.well-known/jwks.json), `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](/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.
