# Emailed links

> The flows that continue from an emailed link: verifying an address, resetting a password and accepting an invitation, and how each returns to the application.

Three flows continue from a link in an email: verifying an address, resetting a password
and accepting an administrator's invitation. All need an [email relay](https://www.nauthera.io/docs/v1/email)
for the organization. The token is in the link's fragment, which never reaches a server log
or a `Referer`, and the page removes it from the address bar as it opens.

A verification or reset link sent while the user was signing in to a registered client, or
connecting a device at `/device`, carries that request, so finishing the flow leads back to
the sign-in it interrupted. Any other link is bare, and ends by telling the user to go back
to the app they came from.

## Verify an address

```mermaid
flowchart LR
  S[Sign-up or sign-in, address unverified] --> P["Verify your email"]
  P -->|Resend| P
  M[Emailed link] --> V[Verifying spinner]
  V -->|valid| OK[Email verified]
  OK -->|Continue| L["/login for the same application"]
  V -->|used or expired| X[Link expired]
  L --> CB([Back to the application])
```

When an organization requires verified addresses, signing up or signing in with an
unverified one does not fail: it lands on **Verify your email**, which names the address
the link went to.

<StoryScreenshot id="screens-verify-email--pending" alt="Verify your email: we sent a verification link, with Resend and Back to sign in" />

**Resend verification email** sends another link. Unless `auth.captcha.provider` is
`none`, it solves a captcha first, and only while a resend is on offer: not while a link is
being spent, nor once the address is verified. If the resend is refused, the reason shows
above the button.

<StoryScreenshot id="screens-verify-email--resend-failed" alt="We couldn't send another link, with the reason above Resend" />

Opening the link spends it on load. While it does, the page shows a spinner; then it says
whether it worked.

<StoryScreenshot id="screens-verify-email--verifying" alt="Verifying your email…" />
<StoryScreenshot id="screens-verify-email--success" alt="Email verified, with Continue" />
<StoryScreenshot id="screens-verify-email--expired" alt="Link expired: request a new one, with Resend and Back to sign in" />

**Continue** opens the application's sign-in, and signing in returns to the application
with its `state`. A link that was used already, or has expired, says so. In the tab that
asked for it, which knows the address, it offers **Resend**; opened in another browser or
on another device it offers only the way back to sign in, where signing in sends a fresh
link. A link verifies only the address it was sent to.

## Reset a password

```mermaid
flowchart LR
  L["/login"] -->|Forgot password?| F["/forgot-password"]
  F -->|Send reset link| SENT[Check your email]
  M[Emailed link] --> R["/reset-password"]
  R -->|new password| D[Password updated]
  D -->|Sign in| L2["/login for the same application"]
  R -->|used or expired| I[This link is no longer valid]
  I -->|Request a new link| F
```

<StoryScreenshot id="screens-forgot-password--default" alt="Forgot your password? Enter your email and we'll send a link" />
<StoryScreenshot id="screens-forgot-password--sent" alt="Check your email: we've sent a password reset link" />

The confirmation reads the same whether or not the address has an account, so the page is
no way to find out who signs in here. The request solves the captcha like a sign-up.

<StoryScreenshot id="screens-reset-password--default" alt="Set a new password, typed twice" />
<StoryScreenshot id="screens-reset-password--with-field-error" alt="The server refused the new password: its reason on the field and in the banner" />
<StoryScreenshot id="screens-reset-password--done" alt="Password updated, with Sign in" />
<StoryScreenshot id="screens-reset-password--invalid-token" alt="This link is no longer valid, with Request a new link" />

An account that waits on an [invitation](#coming-back-without-the-link) is sent a new
invitation instead of a reset link. The confirmation, the same for every address, says
*If you were invited and haven't chosen a password yet, you'll get a new invitation
instead.*

Setting the password spends the link and every other reset link the account holds,
retires every refresh token the user holds, ends the user's sessions, and clears a lock
from failed sign-ins. **Sign in** resumes the application the reset interrupted.

## Accept an invitation

An administrator invites a person from the [admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard):
**Send an invitation** when creating a user, or **Send a new invitation** on the page of a
user who has not chosen a password yet. The account is created without a password, and
the person opens the emailed link and chooses one; following it also verifies the address.
The organization needs an email relay, and a link works for `auth.invitation.validity`
(`168h` by default).

<StoryScreenshot id="screens-accept-invitation--default" alt="Choose your password: you've been invited" />
<StoryScreenshot id="screens-accept-invitation--done-no-app" alt="Your account is ready, with the hint to go back to the app" />

An administrator's invitation names no application, so the done screen tells the person to
go back to the app they were invited to and sign in there. A new invitation does not
cancel an earlier link, but using any one of them spends them all.

A password the server refuses, such as a common one or, with
`auth.password.breach_lookup.enabled`, one found in a breach, or a throttle, leaves the
form and the link as they were: the same link works for another try, now or later.

<StoryScreenshot id="screens-accept-invitation--password-refused" alt="The chosen password was refused: the form stays, and the invitation is not spent" />

### Coming back without the link

An account waits on an invitation while it has no password and no linked
identity-provider account. Its owner does not need the original email to get in: every
other way back sends a new invitation.

```mermaid
flowchart TD
  I([Invited, link never opened]) --> W{What the user tries}
  W -->|Sign in| SI["401: incorrect email or password,<br/>with the invitation hint"]
  W -->|Create account| SU["409: an account already exists,<br/>with the invitation hint"]
  W -->|Forgot password?| FP["202: check your email"]
  W -->|Resend verification| RV["202"]
  W -->|An unusable link| X["This invitation is no longer valid"]
  X -->|Request a new link| FP
  SI & SU & FP & RV -.->|after the response| N["New invitation email"]
  N --> A["/accept-invitation"]
  A --> D[Account ready]
  D -->|the link named an app| APP([Sign in, back to the application])
```

- **Signing in** with any password is refused as a wrong password, and the sign-in form
  adds *If you were invited and haven't chosen a password yet, check your email: we've
  sent you a new invitation.*
  ([Sign in](https://www.nauthera.io/docs/v1/sign-in-flows/sign-in#when-it-does-not-work)).
- **Signing up** with the address is refused as for any address that has an account, and
  the message adds *If you were invited, check your email for a new invitation.*
- **Forgot password?** and **Resend verification** answer as for any address. The
  account is sent an invitation in place of a reset or verification link, since only the
  invitation gets it a password.
- **An unusable link**, one that has expired, was replaced, or was spent, says *This
  invitation is no longer valid* and offers **Request a new link**, which opens
  `/forgot-password`. An account that still waits on an invitation is sent one from there,
  and an account whose owner has chosen a password by now is sent a reset link.

<StoryScreenshot id="screens-accept-invitation--invalid-token" alt="This invitation is no longer valid, with Request a new link" />

Every answer is the one any other address gets, and the invitation is sent after the
response, so none of these pages reveals whether an address waits on an invitation. The
hints are shown for every refused password and every taken address for the same reason.

An invitation the user asked for this way names the organization as the inviter, and its
link carries the authorization request the user started from: after choosing a password
they sign in and go back to the application. Each one counts against
`auth.invitation.max_requests` (`5` per validity window by default) together with the
administrator's; beyond it nothing is sent, and the request is recorded as
`rate_limited`. An account linked to an identity provider, or one that is suspended or
blocked, is sent nothing.

Each send is recorded as `user.invitation.sent`, beside the record of what the user asked
for:

| The user tried | Recorded as | Plus |
|---|---|---|
| Sign in | `auth.sign_in.completed`, failure `invalid_credentials`, cause `password_not_set` | `user.invitation.sent` |
| Forgot password | `auth.password_reset.requested`, failure `conflict`, cause `password_not_set` | `user.invitation.sent` |
| Resend verification | `auth.email_verification.requested`, failure `conflict`, cause `password_not_set` | `user.invitation.sent` |
| Sign up | `auth.registration.completed`, failure `conflict`, cause `identifier_taken` | `user.invitation.sent` |
