# Organizations

> Organizations are Nauthera's tenants: each is its own issuer with its own discovery document, keys, users and sessions, and can be declared as a manifest.

An **organization** (org) is a tenant. Its users, sessions, OAuth clients, signing keys
and audit events belong to it, and it is addressed by a slug.

- **One issuer per org.** The main org is served at the root of the server. Every other
  org is served under `/orgs/{slug}`. Each has its own issuer URL, discovery document
  and JWKS.
- **Isolated identities.** An email address is unique within an org, not across the
  server. Sessions and single sign-on never cross orgs.
- **Users sign in at their org's address.** Nothing routes a user who types only an email
  address to the right org. Letting an org claim and verify its email domains, and
  sending an email-first sign-in to the org that owns the domain, is planned.
- **Per-org keys.** Every org other than the main one gets its own ES256 and RS256
  signing keys when it is created. They are stored in the database, encrypted under the
  deployment's key-encryption key (`keys.encryption_key`). The main org signs with the
  keys configured under `keys.*` and never uses the key-encryption key. See
  [Keys & Secrets](https://www.nauthera.io/docs/v1/keys-and-secrets).
- **Per-org policy.** Each org has its own FAPI mode, verified-address requirement,
  [second-factor policy](#second-factor-policy-and-network-zones), network zones and
  default branding, and can be created in FIPS mode (see below).

## The main org

The main org has the fixed slug `default` (`orgs.default_slug`) and cannot be deleted.
It governs the rest. Through the admin API, listing orgs needs the `orgs.read` permission
and creating, changing and deleting them needs `orgs.write`, each held through a
platform role in the main org; only `platform-owner` holds `orgs.write`. The gRPC
`AdminService` can also create, read, list, change and delete orgs. It does not check
roles: a caller needs the `organizations` operation and the org's slug in its
`server.grpc.tls.allowed_clients` entry (see
[the control plane](https://www.nauthera.io/docs/v1/architecture#the-control-plane)). The main org is served
at the root only, so `/orgs/default/…` answers `404 unknown_org`.

The main org is configured with the deployment, under `orgs.main`. Each setting given is
applied at every startup, and the admin API refuses to change it; a setting left out
stays editable. With the Helm chart:

```yaml
config:
  orgs:
    main:
      display_name: Acme Platform
      fapi_mode: true
      email_verification_required: true
      branding: |
        productName: Acme
        logoUrl: https://acme.example/logo.svg
```

`branding` is text (YAML or JSON) because the configuration loader lowercases the keys of
a nested object. The main org's FAPI mode is read only at startup, so this is the way to
change it. No resource and no control-plane call can create, change or delete the main
org. See [Configuration](https://www.nauthera.io/docs/v1/configuration).

## URLs

For an issuer `https://auth.example.com` and an org `acme`:

| | Main org | Org `acme` |
|---|---|---|
| Issuer | `https://auth.example.com` | `https://auth.example.com/orgs/acme` |
| Discovery | `/.well-known/openid-configuration` | `/orgs/acme/.well-known/openid-configuration` |
| JWKS | `/.well-known/jwks.json` | `/orgs/acme/.well-known/jwks.json` |
| Authorize | `/authorize` | `/orgs/acme/authorize` |
| Token | `/token` | `/orgs/acme/token` |

Org discovery is also served at the RFC 8414 path-inserted form,
`/.well-known/openid-configuration/orgs/acme` and
`/.well-known/oauth-authorization-server/orgs/acme`.

## Creating an org

Through the [admin API](https://www.nauthera.io/docs/v1/user-management), as a main-org caller holding
`orgs.write` (the `platform-owner` role):

```bash
curl -s https://auth.example.com/v1/admin/orgs \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"slug": "acme", "display_name": "Acme Corp"}'
```

The slug must match `^[a-z0-9][a-z0-9-]*$` and be at most 64 characters. The body can
also set `fips_mode`, `fapi_mode`, `email_verification_required`, `branding`,
`second_factor` and `network_zones`. The response contains the org's `issuer` and,
**only this once**, `admin_m2m_secret`, which is the secret of the org's admin machine
client.

The CLI (`create org`, `get org`, `update org`, `delete org`) calls the gRPC
`AdminService`. `create org` takes `--display-name`, `--fapi-mode`,
`--require-email-verification`, `--fips-mode`, `--second-factor-file` and `--zone`; see
[nauthera-cli](https://www.nauthera.io/docs/v1/cli).

```bash
go run ./cmd/nauthera-cli create org acme --display-name "Acme Corp"
```

An org created this way gets no `admin_m2m_secret`. The gRPC `CreateOrganization` call
returns none, so nobody holds the secret of the org's `{slug}-admin-m2m` client and that
client cannot be used.

After creation, `PATCH /v1/admin/orgs/{slug}`, the gRPC `UpdateOrganization` call and
`nauthera-cli update org` change the display name, FAPI mode, verified-address
requirement, second-factor policy and network zones, and the first two also the default
branding. The slug and FIPS
mode are fixed at creation. A change reaches every replica within 30 seconds.

## Declaring an org as a manifest

With the [operator](https://www.nauthera.io/docs/v1/operator), an org can be declared as a cluster-scoped
[`Organization`](https://www.nauthera.io/docs/v1/organization-resource) resource:

```yaml
apiVersion: nauthera.io/v1alpha1
kind: Organization
metadata:
  name: acme
spec:
  slug: acme
  displayName: Acme Inc.
  namespaces: [acme]          # the namespaces whose resources configure acme
  emailVerificationRequired: true
```

The org's clients, identity providers, directories, brandings and broker memberships are
declared in the namespaces it lists in `namespaces`; see
[Which organization a resource configures](https://www.nauthera.io/docs/v1/operator#which-organization-a-resource-configures).

The operator creates the org, or adopts the one that already holds the slug, and writes
the settings in which it differs. Its identity needs the `organizations` operation and
the slug in the server's `allowed_clients`, which the Helm chart's default does not
grant. Deleting the resource leaves the org serving unless it was created with
`deletionPolicy: Delete`. An org created this way gets no `admin_m2m_secret`, as with
the CLI.

## Brokering other orgs

An org can sign in the users of other orgs through its own clients, as an
[identity broker](https://www.nauthera.io/docs/v1/identity-broker): each member org accepts the broker, and
the broker routes an email address to the member that verified its domain.

## Identity providers and sign-up

An org can let its users sign in and sign up through external OpenID Connect providers,
such as Google or Entra ID, or through GitHub. Each provider belongs to one org and is
registered with an [`IdentityProvider`](https://www.nauthera.io/docs/v1/identity-provider-resource) resource
or `nauthera-cli create identity-provider`. A provider is offered only to the clients that
list it, each saying whether it allows sign-in, sign-up or both. The
[admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard) lists an org's providers. See
[Social sign-in](https://www.nauthera.io/docs/v1/sign-in-flows/social-sign-in). An org's LDAP directory or
Active Directory works the same way: an
[`LDAPDirectory`](https://www.nauthera.io/docs/v1/ldap-directory-resource) lets its users sign in, or sign up,
with their directory password on the clients that list it.

Self-service sign-up, with a password or through a provider, is a deployment-wide
setting, `auth.registration.enabled`, off by default. With
`email_verification_required` (`emailVerificationRequired` on the resource), an account
whose address is not verified cannot finish signing in until it follows an emailed link,
which needs the org to have an [email relay](#email). See
[Sign up](https://www.nauthera.io/docs/v1/sign-in-flows/sign-up).

## Second-factor policy and network zones

Each org has a second-factor policy that narrows the deployment's `mfa` policy and that
each of its clients can narrow further. It says which methods (`totp`, `passkey`) users
may use, whether a factor is required, whether a passkey alone signs a user in, how long
a browser that passed a factor is spared it, and rules that narrow it for sign-ins
outside or inside named network zones, from a new network or from a new browser. A
policy that states more than the deployment allows is refused. The org's
[admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard) runs under the org's policy and is never
spared the challenge.

An org can name its own network zones (address ranges) beside the deployment's
`security.network_zones`, for its rules and its clients' rules to use. It may not reuse
a deployment zone's name, and only deployment zones can spare a challenge. See
[Multi-Factor Authentication](https://www.nauthera.io/docs/v1/mfa).

## Admin clients

Every org gets two reserved OAuth clients so that people and automation can reach its
admin API:

| Client ID | Type | Use |
|---|---|---|
| `{slug}-admin-console` | Public, PKCE required, `authorization_code` | An administrator signs in interactively, as the [admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard) at `{issuer}/admin/` does. The admin API reads that user's roles in the org from the database on every call. An org's own users can hold its org roles (`org-viewer` to `org-owner`); platform roles count only in the main org. |
| `{slug}-admin-m2m` | Confidential, `client_credentials` | Automation. Its org and admin roles are fixed when it is provisioned. |

For the main org, the M2M secret is written **once**, when the client is first created,
to `auth.admin.bootstrap_secret_path` (mode `0600`, never overwritten), and never to a
log. That path is **empty by default**, and then the secret is discarded and cannot be
recovered or rotated. Set it before the server's first start if you want this
credential; read the file, store the secret, then delete the file.

Email addresses listed in `auth.admin.subjects` are granted `platform-owner` in the main
org at startup. Each address is granted once, to the main-org account that holds it on the
first start that finds one. Removing an address later never demotes anyone. See
[Admin API & Users](https://www.nauthera.io/docs/v1/user-management#getting-a-token).

## Email

An org sends email for [new-sign-in alerts](https://www.nauthera.io/docs/v1/sessions#new-sign-in-alerts),
password reset links, email verification links, invitations,
[emailed sign-in codes](https://www.nauthera.io/docs/v1/mfa#emailed-code), notices of a changed password,
second factor or recovery codes, role grants and revocations, and a suspended, blocked or
reactivated account, in the org's branding and the recipient's language. It sends through
its own SMTP relay when it has one, and otherwise through the deployment's
(`mail.smtp.*`). An org with neither sends no email, and those features stay off for it.

Per-org relays are configured with the deployment, under `mail.organizations`, by slug.
With the Helm chart, list them in `mailOrganizations`; each entry names an existing
Secret with the relay's credentials, which the chart mounts as files:

```yaml
mailOrganizations:
  - slug: acme
    smtp:
      host: smtp.acme.example
      port: 587
      tls: starttls
    from:
      address: no-reply@acme.example
      name: Acme
    credentials:
      existingSecret: acme-smtp
```

The admin dashboard's **Email** page shows which relay an org uses and sends a test
message. [Email](https://www.nauthera.io/docs/v1/email) describes relays, TLS and the outbox in full.

## FAPI mode and FIPS mode

`fapi_mode` can be set when an org is created and changed afterwards; `fips_mode` is
fixed at creation:

- **`fapi_mode`** applies part of the FAPI 2.0 Security Profile to the org. It requires
  pushed authorization requests and PKCE with S256, signs only with ES256, and caps the
  authorization code lifetime at 60 seconds. It does **not** refuse shared-secret
  clients or require sender-constrained tokens.
- **`fips_mode`** hashes passwords and recovery codes with PBKDF2-HMAC-SHA-256 instead
  of argon2id. It needs the Go FIPS 140-3 module on and pinned to a frozen version, as in
  a binary built with `GOFIPS140=v1.0.0`; an ordinary build run with
  `GODEBUG=fips140=on` does not count. On any other process the org fails closed:
  creating it fails with `409` over the admin API, or `FAILED_PRECONDITION` over gRPC,
  and an org that already exists is not served.

## Deleting an org

`DELETE /v1/admin/orgs/{slug}`, the gRPC `DeleteOrganization` call, or deleting an
`Organization` resource under `deletionPolicy: Delete` removes an org other than the main
one. The replica that deleted it stops serving it at once, and the others within 30
seconds. Only the org's own record is deleted, with its second-factor policy, network
zones and queued email: its users, clients and sessions stay in the database. The
reserved admin clients left behind also hold the slug: creating an org with the same slug
again is refused with `409` (`FAILED_PRECONDITION` with the reason
`RESERVED_CLIENT_HELD` over gRPC), naming the leftover client to remove first.
