Metadata-Version: 2.4
Name: splent_feature_reset
Version: 2.2.0
Author-email: DiversoLab <diversolab@us.es>
License-Expression: LGPL-3.0-or-later
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: COPYING
License-File: COPYING.LESSER
Dynamic: license-file

# splent_feature_reset

The `reset` feature of a [SPLENT](https://splent.io) product line.

A password reset by mail. Without it a person who forgets their password
needs an administrator. With it, a "Forgot your password?" link appears
under the login form and the sign-up form, and leads to a page that mails a
link to set a new password. It needs `auth`, which owns the account, and
`mail`, which sends the link.

A SPLENT feature is a Python package that a product composes with others.
Its models, routes, services, templates and hooks are registered when the
product starts. What this feature provides and requires is listed in the
`[tool.splent.contract]` section of `pyproject.toml`, which
`splent feature:contract --write` keeps in sync with the source.

## What a reset does

1. `/reset/forgot` asks for an address. On submit, `reset-submitting` is
   sent, and any listener may refuse (see below).
2. The address finds the account by its canonical form (auth compares it
   byte for byte, so an accented look-alike finds nothing), and the link
   goes to the address the account has, never to the one typed. A switched
   off account gets no link. The page answers the same whether or not the
   address has an account, so it does not tell anyone which addresses are
   registered.
3. The link carries one of auth's one-time tokens (auth 1.22 or later): 32
   random bytes, of which auth keeps only the SHA-256, with the account, the
   address it went to and its expiry. `/reset/password/<token>` opens for
   `RESET_TOKEN_MAX_AGE` seconds (an hour). It asks for the new password
   twice and follows auth's password policy, the same as every other form
   that sets a password (with auth 1.23, its breach check too). The page
   sends no Referer, is not cached and, with auth 1.23, runs no script of
   another site.
4. In one transaction the link is spent, with the account's other reset
   links, the password changes, every session of the account ends and the
   address is marked confirmed, since the link reached it. A mail tells the
   account its password was changed.

| Case | Result |
|---|---|
| A link used, expired, of the old signed kind, or not a link at all | `410`, a page that says so and asks for a new link |
| The same link followed twice at once | One sets the password, the other gets the `410` page |
| A password the policy refuses, or two that differ | The page again, with why, and `400` |

The texts are translated to Spanish.

## Password

| Setting (auth) | Default | On the reset page |
|---|---|---|
| `AUTH_PASSWORD_MIN_LENGTH` | 8 | The shortest password accepted |

The page always asks twice, whatever `AUTH_PASSWORD_CONFIRM` says. With an
auth older than its password policy, which has no `passwords` module, the
page asks for at least 8 characters.

## Checks before a link is sent

`reset-submitting` is sent when someone asks for a link, before the address
is looked up and before any mail goes out, with `form` (the posted form) and
`remoteip` (the visitor's address). A listener that returns `False` refuses
the request: no mail goes out, and the form comes back with the address
typed and "We could not verify this request. Please try again.", which says
nothing about the account. `None` or `True` let it through.

A listener draws what it needs in the `reset.form_fields` slot, inside the
form and right above the button. A callback takes no arguments and returns
HTML, or an empty string to add nothing on this request.

`splent_feature_captcha` listens here and fills the slot, once the product
names the form:

```toml
[tool.splent.config]
CAPTCHA_PROTECTED_FORMS = ["signup", "reset"]
```

A feature of its own listens the same way:

```python
# signals.py of a feature that checks the request
from splent_framework.signals.signal_utils import connect_signal


@connect_signal("reset-submitting", "splent_feature_mycheck")
def check_the_request(sender, form=None, remoteip=None, **kwargs):
    return form.get("my-answer") == "..."
```

The signal and the slot exist from the release after 1.5.0. With 1.5.0 or
older the form cannot be protected, and naming `reset` in
`CAPTCHA_PROTECTED_FORMS` protects nothing (captcha's log says so).

## Slots

| Slot | Opened by | What reset puts there |
|---|---|---|
| `reset.form_fields` | The page that asks for a link | Nothing. It is for a check's widget |
| `auth.login.form_footer` | auth's login form | "Forgot your password? Click here!" |
| `signup.form_footer` | signup's form | The same link |
| `layout.scripts` | The layout | `reset.bundle.js`, which keeps the button of the new password off until both boxes match |

## Not yet

- Nothing limits how many links one address can ask for, apart from a
  check on `reset-submitting`, such as a captcha. A throttle per address and
  per account, with the same answer either way, is still to come.
- The texts of the pages and the mails are in English, with no translations.

## Use it in a product

```bash
splent feature:add splent_feature_reset
```

## Develop

```bash
splent feature:test splent_feature_reset
splent feature:contract splent_feature_reset
```

## Release

```bash
splent feature:release splent_feature_reset --dry-run --bump minor
splent feature:release splent_feature_reset
```

The first command rehearses the release and changes nothing. The second
publishes it to the git forge and to PyPI, and a PyPI version is permanent.
