# Platform sign-in

Let your people open your customer workspaces in eSignStack with a login link from your app or with your own OpenID Connect provider.

A platform has two ways to sign its people in without a separate eSignStack password:

- **Login links:** your server asks for a single-use link for one person and one workspace, then redirects them to it.
- **Sign in with your platform (OIDC):** you register your OpenID Connect provider, and people sign in with their platform account.

Both identify a person by your own ID for them, never by email. Both give people **managed memberships** in your workspaces: memberships that follow what you send and are never mixed up with memberships added by hand.

> [!NOTE]
> eSignStack turns on platform sign-in for your platform. Until it's on, login link requests return `403` and your sign-in page isn't available.

## Login links

Call with a platform key that has the `login_links:write` ability:

```http
POST /api/v1/login_links
Authorization: Bearer sk_live_…
Content-Type: application/json

{
  "workspace": "ws_01J8Z3Q4M7X2V5N9K6T1R0WABC",
  "actor": {
    "external_id": "user_4821",
    "email": "jane@example.com",
    "name": "Jane Doe",
    "role": "admin",
    "email_verified": true
  },
  "return_to": "/acme-dental/envelopes"
}
```

| Field                  | Notes                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `workspace`            | A workspace your platform owns, in the same mode as the key.                                            |
| `actor.external_id`    | Your ID for the person. Required.                                                                       |
| `actor.email`          | Required.                                                                                               |
| `actor.role`           | The workspace role: `admin`, `developer` or `member`. Never `owner`.                                    |
| `actor.email_verified` | Set `true` only if you verified the address yourself. Defaults to `false`.                              |
| `return_to`            | Optional path in eSignStack to land on, such as `/acme-dental/envelopes`. Full URLs are rejected.        |

The response has a single-use `url` that expires after **60 seconds**. Redirect the browser to it right away. Without `return_to`, the person lands on the workspace dashboard.

### First sign-in

- **New email:** we create an eSignStack account and add the person to the workspace with the role you sent. Later links update the role. Without `email_verified: true`, the account's email stays unverified and envelopes already sent to that address aren't linked to it.
- **Email already has an eSignStack account:** nothing is linked silently. If you sent `email_verified: true`, we email that address a confirmation link (valid for 60 minutes); once the account holder confirms, the sign-in is linked. If you didn't, the sign-in is refused.
- **Existing account whose email was never verified:** confirming takes the account over. Its password stops working, two-factor authentication is removed and its other sessions end.

### Remove a person

```http
POST /api/v1/login_identities/user_4821/deprovision
Authorization: Bearer sk_live_…
```

This removes every membership that login links created for `user_4821` and ends their platform sessions at once. Their eSignStack account, workspaces of their own and your workspaces' documents are untouched. A later login link signs them in again.

## Sign in with your platform (OIDC)

### Register your provider

A platform admin opens **Sign-in (identity provider)** in the [platform console](/docs/platform-console) and fills in:

- **Display name** and an optional **Logo URL**, shown on your sign-in page.
- **Issuer**: your HTTPS issuer URL, exactly as it appears in your discovery document. We read the discovery document and signing keys when you save, and pin them.
- **Client ID** and **Client secret**.
- **Tenant claim** (default `tenants`): the ID token claim that lists the person's workspaces.
- **Role mapping**: one `your role=workspace role` per line, such as `director=admin`. Workspace roles are `admin` or `member`.
- **Platform workspaces require a sign-in through this provider within the last 12 hours** (on by default).
- **Platform role claim** (optional): a claim that grants console roles (`admin`, `developer`, `billing` or `support`). Each grant is audited.
- **Console MFA** (optional): accepted `amr` and/or `acr` values. When set, platform admins must sign in through your provider with a matching value to open the console.

After saving, the page lists what to give your provider and where to link people:

| Setting                  | Value                                         |
| ------------------------ | --------------------------------------------- |
| Redirect URI             | `https://esignstack.app/sso/{provider}/callback`           |
| Back-channel logout URI  | `https://esignstack.app/sso/{provider}/backchannel-logout` |
| Start sign-in            | `https://esignstack.app/sso/{provider}/start`              |
| Your sign-in page        | `https://esignstack.app/login/{your-platform}`             |

**Disable** turns the provider off. You can also manage it with the API (`/identity_providers`, live platform key with `identity_providers:write`).

### The sign-in flow

We use the authorization code flow with PKCE (S256), `state` and `nonce`, scope `openid email profile`, and send the client secret in the token request body. A sign-in must finish within 10 minutes of starting.

The ID token must:

- be signed with RS256 or ES256 by a key in your published key set (`none` and HMAC are refused);
- have `iss` exactly equal to the registered issuer, and `aud` containing the client ID (with `azp` equal to it when there are several audiences);
- pass `exp`, `iat` and `nonce` checks, allowing two minutes of clock skew;
- include `email` and `email_verified` as the boolean `true`.

A person is identified by your issuer and their `sub`. The same account rules as login links apply: an existing account with the same email is linked only after its holder confirms by email.

### Claims

| Claim                    | Used for                                                                 |
| ------------------------ | ------------------------------------------------------------------------ |
| `sub`                    | The person's ID. Required.                                               |
| `email`, `email_verified` | Account email. `email_verified` must be `true`.                        |
| `name`                   | Display name for a new account.                                          |
| Tenant claim             | `[{ "external_id": "customer_1001", "role": "director" }]`              |
| `sid`                    | Lets back-channel logout end one session.                                |
| `amr`, `acr`             | Checked against **Console MFA**, if set.                                 |
| Platform role claim      | Grants a console role, if configured.                                    |

Each `external_id` matches only a workspace of your platform with that external ID. Each role goes through the role mapping; `admin` and `member` pass through unmapped, any other unmapped role grants nothing, and `owner` is never granted.

### Membership sync

Memberships are synced at every OIDC sign-in: added, role-updated, or removed when the tenant claim no longer lists the workspace. Memberships added by hand, or by login links, are never touched. A person with no workspace who isn't a platform member is refused.

### Sign-out and removal

- **Back-channel logout:** when your provider posts a logout token to the back-channel logout URI, we end the matching platform sessions at once (by `sid`, or every session for the `sub`).
- **Deprovision:** call `POST /api/v1/identity_providers/{provider}/subjects/{sub}/deprovision` with a live platform key. It removes the memberships your provider manages for that person and ends their platform sessions.

## Recent platform sign-in

By default, your workspaces are reachable only in a session started through your platform (a login link or your provider) in the last 12 hours, so a person you remove loses access quickly. Sessions from a password or Google sign-in don't count. When a session gets too old and you have a provider, eSignStack sends the browser back through it to sign in again.

If you've registered a provider, you can let people use your workspaces with a password or Google sign-in too: clear **Platform workspaces require a sign-in through this provider within the last 12 hours**. Workspaces people own themselves are never affected.

## Audit log

Each step is recorded in the console's **Audit log**, including `sso.login_link_issued`, `sso.signed_in`, `sso.user_provisioned`, `sso.membership_granted`, `sso.membership_role_changed`, `sso.membership_revoked`, `sso.account_link_requested`, `sso.account_link_confirmed`, `sso.platform_role_granted`, `sso.sessions_revoked`, `sso.deprovisioned`, `sso.sign_in_refused` and `identity_provider.updated`.
