|
| 1 | +# Changelog |
| 2 | + |
| 3 | +Notable changes to django-mfa, newest first. |
| 4 | + |
| 5 | +This file starts at 4.1.0. For the 2.x/3.x → 4.0 rewrite — which was a |
| 6 | +ground-up rebuild with breaking changes to models, URLs, session keys and |
| 7 | +settings — see [docs/upgrading.md](docs/upgrading.md); it is far more than a |
| 8 | +changelog entry could carry. Releases before 4.1.0 are on the |
| 9 | +[GitHub releases page](https://github.com/MicroPyramid/django-mfa/releases). |
| 10 | + |
| 11 | +Versions follow [PEP 440](https://peps.python.org/pep-0440/). The version in |
| 12 | +`pyproject.toml` is the only place it is written; the git tag and the GitHub |
| 13 | +Release are derived from it (see [docs/contributing.md](docs/contributing.md)). |
| 14 | + |
| 15 | +## 4.1.0 |
| 16 | + |
| 17 | +Three additions, all opt-in. **An install that sets none of the new settings |
| 18 | +behaves identically to 4.0.1** — the only required step is running the new |
| 19 | +migration. |
| 20 | + |
| 21 | +### Added |
| 22 | + |
| 23 | +- **`MFA_REQUIRED` — require a second factor.** Until now enrollment was |
| 24 | + entirely voluntary: the middleware only challenged users who had *already* |
| 25 | + enrolled, so anyone who never opted in was never prompted, and there was no |
| 26 | + way to require MFA of staff. Accepts `False` (default), `True`, a callable |
| 27 | + taking a user, or a dotted path to one; `django_mfa.policy` supplies |
| 28 | + `is_staff` and `in_groups(*names)`. A required user holding no primary factor |
| 29 | + is walled to the security page — only the enroll pages, recovery codes and |
| 30 | + `MFA_EXEMPT_PATHS` stay reachable — until they enroll. See |
| 31 | + [docs/enforcement.md](docs/enforcement.md). |
| 32 | +- **`@mfa_required` and `MfaRequiredMixin`** (`django_mfa.decorators`) for |
| 33 | + per-view enforcement regardless of `MFA_REQUIRED`. The setting picks users, |
| 34 | + the decorator picks views, and neither can express the other. |
| 35 | +- **System check `django_mfa.E004`**, rejecting an unimportable or |
| 36 | + non-callable `MFA_REQUIRED` at `manage.py check` rather than from inside |
| 37 | + middleware on a user's first live request. Unlike E001–E003 it is not gated |
| 38 | + on WebAuthn being active. |
| 39 | +- **An emailed one-time-code factor** (`"email"`) — the only built-in that does |
| 40 | + not assume the user still holds a device they enrolled earlier, which makes |
| 41 | + it the lost-phone path. **Not in the `MFA_FACTORS` default**: add it |
| 42 | + explicitly, so that upgrading cannot silently acquire a factor that sends |
| 43 | + mail through a backend this package does not control. New settings |
| 44 | + `MFA_EMAIL_CODE_LENGTH` (6), `MFA_EMAIL_CODE_VALIDITY` (300s), |
| 45 | + `MFA_EMAIL_SEND_RATE_LIMIT` (`"3/5m"`), `MFA_EMAIL_SUBJECT`, and |
| 46 | + `MFA_FROM_EMAIL`. |
| 47 | +- **Five signals** — `factor_added`, `factor_removed`, `mfa_verified`, |
| 48 | + `mfa_verification_failed`, `recovery_code_used` — importable from |
| 49 | + `django_mfa.signals`, always on. All sent with `send_robust()`, so a raising |
| 50 | + receiver cannot break a security action such as removing a compromised key. |
| 51 | + `mfa_verification_failed` also fires for attempts the rate limiter refuses: a |
| 52 | + brute-force detector needs the refused attempts, not only the evaluated ones. |
| 53 | + See [docs/api.md](docs/api.md). |
| 54 | +- **`MFA_NOTIFY_ON_CHANGE`** (default `False`) — emails the user when a factor |
| 55 | + is added or removed, when a recovery code is spent, and when their last |
| 56 | + factor goes. Sending is synchronous and best-effort: a failure is logged, |
| 57 | + never raised, because a mail outage must not turn "remove this key I think is |
| 58 | + compromised" into a 500. For async delivery or non-email routing, connect |
| 59 | + your own receiver to the signals above and leave this off — that is why the |
| 60 | + signals ship independently of the emails. |
| 61 | +- **`Registry.has_primary_factor(user)`** — the boolean form of |
| 62 | + `primary_enabled_for()` in one query instead of one per registered adapter. |
| 63 | + Both derive from `Adapter.counts_as_primary_factor`, so they cannot disagree. |
| 64 | +- New documentation page, **Enforcement**. |
| 65 | + |
| 66 | +### Changed |
| 67 | + |
| 68 | +- `Adapter.complete_enroll()` **must return the created `Authenticator`**. This |
| 69 | + was always true of the built-ins, but it is now a documented contract: the |
| 70 | + `factor_added` signal carries the return value, so a custom adapter returning |
| 71 | + `None` silently degrades every host project's audit trail for that factor |
| 72 | + type. |
| 73 | +- `ratelimit.parse/check/record_failure` gained a `setting=` keyword argument so |
| 74 | + a second budget (emailed-code sends) can be counted against its own setting. |
| 75 | + Existing positional calls are unaffected; `record_failure` is now an alias of |
| 76 | + the more general `record`. |
| 77 | +- `docs/custom_factors.md`'s worked example is now a printed-backup-token |
| 78 | + factor. Its previous example was an emailed-code factor, which now ships as a |
| 79 | + built-in, so the page had begun documenting how to reimplement something the |
| 80 | + package provides. |
| 81 | + |
| 82 | +### Upgrading |
| 83 | + |
| 84 | +Run `manage.py migrate django_mfa`. Migration `0008_email_factor` adds the |
| 85 | +`email` factor type and extends the `mfa_one_singleton_authenticator_per_user` |
| 86 | +constraint to cover it — a singleton factor missing from that condition would |
| 87 | +not actually be constrained. Unlike `0007`, it is reversible. |
| 88 | + |
| 89 | +Nothing else is required. `MFA_REQUIRED`, `MFA_NOTIFY_ON_CHANGE` and the |
| 90 | +absence of `"email"` from `MFA_FACTORS`'s default are what keep existing |
| 91 | +behaviour unchanged. |
0 commit comments