# Broker sign-in

> Signing in at an identity broker: the email screen, routing to the member organization, the member's own sign-in, the broker's tokens and every unfinished end.

A client of an [identity broker](https://www.nauthera.io/docs/v1/identity-broker) asks for nothing but an email
address. The broker sends the person to the member organization that verified the
address's domain, the member signs them in with its own screens, and the person comes
back to the application signed in at the broker. The screen appears for a client the
broker's memberships admit; every other client of the same organization keeps the
ordinary [sign-in](https://www.nauthera.io/docs/v1/sign-in-flows/sign-in).

```mermaid
flowchart LR
    AZ["Broker /authorize"] --> E["Email screen (broker's branding)"]
    E -->|"domain verified by a member<br/>admitted for this client"| S["/v1/auth/broker/{member}/start"]
    E -->|"no member"| N["No organization found"]
    S --> M["Member's /login<br/>(member's branding, prefilled)"]
    M --> MF{"Member's next step:<br/>second factor, verification"}
    MF --> CB["/v1/auth/broker/{member}/callback"]
    CB -->|"signed in"| C{"Consent at the broker,<br/>if needed"}
    C --> APP([Back to the application])
    CB -->|"refused or failed"| ER["Email screen with the reason"]
```

## The email screen

<StoryScreenshot id="screens-sign-in-at-a-broker--default" alt="A single email field under the broker's name, with a Continue button" />

The screen wears the broker's branding and asks for a work email address. A `login_hint`
from the application fills it in:

<StoryScreenshot id="screens-sign-in-at-a-broker--prefilled-email" alt="The email field prefilled with alice@acme.example" />

While the address is routed, the form is disabled and the button spins:

<StoryScreenshot id="screens-sign-in-at-a-broker--submitting" alt="The prefilled form, disabled, with a spinner on the button" />

An address the server cannot take is answered under its field:

<StoryScreenshot id="screens-sign-in-at-a-broker--with-field-errors" alt="Enter a valid email address, under the field holding alice@" />

The broker's own branding can stand beside the form, as on every sign-in screen:

<StoryScreenshot id="screens-sign-in-at-a-broker--split-layout" alt="The email form beside the broker's branded panel" />

## Routing to the member

The address goes to the member organization that has verified its domain, when the
membership is active and admits the application's client. The broker then sends the
browser to the member's own `/authorize` with the address as `login_hint`, a fresh
`state`, `nonce` and PKCE challenge, `prompt=login` when the application asked for a fresh
sign-in, and the application's `max_age`.

From there the person is on the **member's** pages: its branding, its password, its
second factor, its passkeys, its lockout. Everything on
[Sign in](https://www.nauthera.io/docs/v1/sign-in-flows/sign-in), [Second factor](https://www.nauthera.io/docs/v1/sign-in-flows/second-factor)
and [Passkeys](https://www.nauthera.io/docs/v1/sign-in-flows/passkeys) applies as it does for the member's own
applications. The member treats the broker's reserved client as its own, so it asks for no
consent; any consent to the application is asked at the broker.

Coming back, the broker checks that the answer is for this browser and this member, and
that the member's `iss`, nonce and `auth_time` hold, then signs the person in to its own
linked account. The application's tokens name the member in `member_org` and
`member_org_slug`.

## No organization found

An address whose domain no member has verified, or one verified by an organization that
is not a member of this broker, ends here. The broker reveals nothing beyond that no
member matched, and offers to try another address:

<StoryScreenshot id="screens-sign-in-at-a-broker--no-organization" alt="No organization found, naming bob@unknown.example, with Use a different email" />

The broker's own users end here too: the broker routes only to members. Give them an
organization of their own that is a member, with its domain verified.

## When it comes back unfinished

A sign-in at the member that comes back refused lands on the email screen again with the
reason as a banner:

<StoryScreenshot id="screens-sign-in-at-a-broker--member-sign-in-refused" alt="Sign-in failed: This app can't sign you in through that organization, above the email form" />

| Reason | What happened |
|---|---|
| `not_allowed` | There is no active membership admitting this client, at the start or on the way back (it ended during the sign-in), or the member's account is already linked to another of the broker's accounts. |
| `failed` | The member's answer could not be accepted: a state from another browser, used twice or expired; the member refused the sign-in; the wrong issuer; a code or ID token that did not verify; or a member sign-in older than the `prompt=login` or `max_age` the application asked for. |

A failure the server answered is shown the same way:

<StoryScreenshot id="screens-sign-in-at-a-broker--with-error" alt="Something went wrong. Try again., as a banner above the email form" />

Every step is in the audit log of both organizations: the routing, the hop, the member's
sign-in, and any refusal with its cause.

## Related

- [Identity broker](https://www.nauthera.io/docs/v1/identity-broker): memberships, domains and tokens
- [BrokerMembership](https://www.nauthera.io/docs/v1/broker-membership-resource): declaring a membership
- [Errors](https://www.nauthera.io/docs/v1/sign-in-flows/errors): the screens every flow shares
