feat: verify email addresses and recover forgotten passwords
Ekklesia could authenticate a person and, since Pass 1, send a transactional email — but it had no way to establish that an address belonged to whoever claimed it, and no way back in for someone who forgot their password. A verified address is now a precondition for holding a session. Better Auth enforces that after the password check and before any session exists, so an unverified sign-in returns 403 with no cookie. That is why no "unverified principal" was needed: a correct password on an unverified address carries exactly the authority of no password, and /api/v1/principal is unchanged. Better Auth keeps every token. Ekklesia adds no token table, no hashing, and no expiry clock. It supplies a recipient, a subject, and two bodies, and the mail provider supplies everything about SMTP. Verification and reset links are rebuilt from a validated Dashboard origin, taking only the token — never the URL Better Auth prebuilds from a caller-supplied callbackURL — so no request can steer where a recipient lands. Following a verification link mints no session, because mail infrastructure dereferences links. Reset revokes that person's other sessions and leaves managed devices alone. Recovery answers identically for an address with an account, without one, already verified, or undeliverable — including when SMTP is broken, which is why the delivery adapter never throws. The initial platform owner is the one exception: created already verified by an operator command with no mail prerequisite, so a fresh server can be signed in to before a relay exists. Migration 0007 extends that to owners of installations that already exist. Public signup stays disabled; verification is not a licence to self-register. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3581ad7c8e
commit
ea28f8aa2e
@@ -3,9 +3,14 @@
|
||||
Ekklesia can send transactional email through a host-configured SMTP transport
|
||||
([ADR 0037](../architecture/decisions/0037-host-provided-outbound-email.md)).
|
||||
|
||||
**Nothing in the platform sends email yet.** This pass added the transport; email verification,
|
||||
password reset, and invitations are Phase 7C Pass 2. An installation with no SMTP configured is a
|
||||
complete, valid deployment — the platform starts and every existing feature works.
|
||||
Two things send email: **email verification** and **password recovery**
|
||||
([ADR 0038](../architecture/decisions/0038-email-verification-and-password-recovery.md)). Invitations
|
||||
do not exist.
|
||||
|
||||
An installation with no SMTP configured is still a complete, valid deployment — the platform starts,
|
||||
every existing feature works, and the initial platform owner can sign in. What it cannot do is verify
|
||||
a new account or recover a password, because both need a message to arrive. See
|
||||
[When no relay is configured](#when-no-relay-is-configured).
|
||||
|
||||
## Configuring a relay
|
||||
|
||||
@@ -121,8 +126,36 @@ transport refusing to continue when the upgrade is not offered.
|
||||
|
||||
**No test sends real email and no live credential exists in this repository or in CI.**
|
||||
|
||||
## Next
|
||||
## What the platform actually sends
|
||||
|
||||
Phase 7C Pass 2 wires Better Auth's `sendResetPassword` and `emailVerification.sendVerificationEmail`
|
||||
hooks — both currently unimplemented — to this transport. See
|
||||
[Identity](identity.md#outbound-email-and-the-verification-seam).
|
||||
| message | when | contents |
|
||||
| --- | --- | --- |
|
||||
| Verify your Ekklesia email address | a new account signs in unverified, or asks for a resend | one link, an expiry sentence, and ignore-if-unexpected wording |
|
||||
| Reset your Ekklesia password | someone asks to recover an account | one link, an expiry sentence, and a note that the current password still works |
|
||||
|
||||
Both are plain text and HTML, both are generic Ekklesia templates with no branding and no marketing
|
||||
content, and both are built in `core/src/identity/notifications.ts` — which knows a recipient, a
|
||||
subject, and two bodies, and nothing about SMTP.
|
||||
|
||||
Links are rebuilt from validated configuration rather than taken from the request that triggered
|
||||
them, so no caller can steer where a recipient lands. They point at
|
||||
`EKKLESIA_DASHBOARD_ORIGIN` (defaulting to the identity base URL). See
|
||||
[Identity](identity.md#email-verification-and-password-recovery).
|
||||
|
||||
## When no relay is configured
|
||||
|
||||
Nothing crashes and nothing changes shape. A verification or recovery request answers exactly as it
|
||||
would with a working relay — the response must not reveal whether an account exists, and it must not
|
||||
start revealing it the moment SMTP breaks — and the operator is told out of band:
|
||||
|
||||
```text
|
||||
[identity] password-reset email was not delivered: MAIL_NOT_CONFIGURED
|
||||
```
|
||||
|
||||
That line carries a flow and a sanitized code. It never carries the address, the token, or the link.
|
||||
`pnpm doctor` reports the same absence under `outbound-email.configuration`.
|
||||
|
||||
The operator requirement is therefore:
|
||||
|
||||
- **normal production use of verification and recovery** → requires configured outbound email
|
||||
- **initial platform-owner bootstrap** (`pnpm identity:create-initial-owner`) → does not
|
||||
|
||||
Reference in New Issue
Block a user