Signing in through the primary address, when the instance's mail works #153

Closed
opened 2026-09-09 11:09:36 +00:00 by tiagoagueda · 1 comment
Owner

Observation

when smtp (server side is configured and working) admin can enable the option to single sign
on using a link sent to the primary email

What exists

The library already does this, and it is switched off. allauth 65.19 is installed and
carries the whole feature:

LOGIN_BY_CODE_ENABLED        LOGIN_BY_CODE_TIMEOUT        LOGIN_BY_CODE_MAX_ATTEMPTS
LOGIN_BY_CODE_REQUIRED       LOGIN_BY_CODE_FORMAT         LOGIN_BY_CODE_MAX_RESEND_COUNT
LOGIN_BY_CODE_TRUST_ENABLED

None of them is set in config/settings/base.py. So this is mostly a decision, a setting and
a page — not an implementation.

The surrounding conditions are already right. ACCOUNT_EMAIL_VERIFICATION = "mandatory",
so a primary address is verified by construction; ACCOUNT_MAX_EMAIL_ADDRESSES = 5;
ACCOUNT_LOGIN_METHODS = {"username", "email"}. Rate limiting already leans on allauth's own
cache-backed limits, which #112 deliberately reused rather than replacing.

And there is a precedent for exactly the hard question this raises.
SiteSettings.sso_is_second_factor — "single sign-on counts as the second factor" — is a
nullable boolean an administrator sets, with a help text explaining what saying yes means.
Somebody arriving by a link in their inbox raises the same question, and it should be
answered the same way rather than by default.

What this asks for

An administrator switch, available when the instance's mail works, that lets people sign in
through their primary address instead of their password.

Worth being careful about

A code is safer than a link, and the reason is not theoretical. allauth's implementation
sends a code the person types back into the session that asked for it. A link is a bearer
credential that works from anywhere, and mail security scanners follow links — corporate
filters, Defender, Proofpoint and their kind fetch every URL in a message, which consumes a
single-use link before the recipient has read the mail. Postulo's users are job applicants
corresponding with corporate recruiters; some of them have exactly that kind of mail. A code
typed back into the browser that asked for it cannot be burned by a scanner, cannot be
forwarded usefully, and cannot be clicked from a device that is not the one signing in. The
suggestion says link; the recommendation is code, and this is the paragraph to argue with.

Does it satisfy the second factor? This is the question that decides whether the feature
is safe or a hole. If somebody with TOTP configured can sign in by a code from their inbox
and skip the second factor, the feature has removed their second factor by adding a first
one. allauth applies MFA after authentication, so the answer is probably no bypass — but
"probably" is not the standard here. It needs a test that says so explicitly, in
tests/security/, of the same kind that already covers the other boundaries.

And whether it counts as a factor is a separate question with an existing answer.
sso_is_second_factor shows the shape: a nullable boolean on SiteSettings, a help text
that says what it means, and no default that quietly decides for the operator. This wants the
same, and probably wants to say no.

It makes email more load-bearing, not less. Today email resets a password; with this it
signs somebody in. Anybody who holds the mailbox holds the account, without needing to
complete a reset. That is not an argument against it — it is already true of password
reset — but it is an argument for the instance being honest about it on the settings page,
and for #104's lock reasoning getting stronger rather than weaker.

"Configured and working" is #152 and cannot be assumed. recovery_routes() counts a
transport that is selected, not one that delivers. Offering sign-in by email on an
instance whose SMTP is misconfigured produces a sign-in page that promises something it
cannot do, to somebody who may have no other way in. The switch should be offered only where
mail is known to work, which is exactly what that issue is for.

The primary address, and only a verified one. Allauth allows up to five addresses; a code
goes to the primary. If the primary changes, the route changes with it, silently — worth
saying on the addresses page rather than leaving somebody to discover it.

Enumeration. The response to "send me a code" must not differ between an address that
exists and one that does not, and the message must say so in a way that is honest rather than
evasive — the same care #90's uniqueness message took.

Rate limits and resend. LOGIN_BY_CODE_MAX_ATTEMPTS and LOGIN_BY_CODE_MAX_RESEND_COUNT
exist; the numbers are a decision, and the failure has to be a refusal that says when to try
again, like the 429 with Retry-After that #112 established.

One more route to explain on a page that already explains several. Settings → Account
already says how sign-in works, with passkeys, TOTP and SSO each described. A fourth way in
needs its sentence there, and the strings are 39 translations.

## Observation > when smtp (server side is configured and working) admin can enable the option to single sign > on using a link sent to the primary email ## What exists **The library already does this, and it is switched off.** allauth 65.19 is installed and carries the whole feature: ``` LOGIN_BY_CODE_ENABLED LOGIN_BY_CODE_TIMEOUT LOGIN_BY_CODE_MAX_ATTEMPTS LOGIN_BY_CODE_REQUIRED LOGIN_BY_CODE_FORMAT LOGIN_BY_CODE_MAX_RESEND_COUNT LOGIN_BY_CODE_TRUST_ENABLED ``` None of them is set in `config/settings/base.py`. So this is mostly a decision, a setting and a page — not an implementation. **The surrounding conditions are already right.** `ACCOUNT_EMAIL_VERIFICATION = "mandatory"`, so a primary address is verified by construction; `ACCOUNT_MAX_EMAIL_ADDRESSES = 5`; `ACCOUNT_LOGIN_METHODS = {"username", "email"}`. Rate limiting already leans on allauth's own cache-backed limits, which #112 deliberately reused rather than replacing. **And there is a precedent for exactly the hard question this raises.** `SiteSettings.sso_is_second_factor` — *"single sign-on counts as the second factor"* — is a nullable boolean an administrator sets, with a help text explaining what saying yes means. Somebody arriving by a link in their inbox raises the same question, and it should be answered the same way rather than by default. ## What this asks for An administrator switch, available when the instance's mail works, that lets people sign in through their primary address instead of their password. ## Worth being careful about **A code is safer than a link, and the reason is not theoretical.** allauth's implementation sends a **code** the person types back into the session that asked for it. A link is a bearer credential that works from anywhere, and mail security scanners *follow links* — corporate filters, Defender, Proofpoint and their kind fetch every URL in a message, which consumes a single-use link before the recipient has read the mail. Postulo's users are job applicants corresponding with corporate recruiters; some of them have exactly that kind of mail. A code typed back into the browser that asked for it cannot be burned by a scanner, cannot be forwarded usefully, and cannot be clicked from a device that is not the one signing in. The suggestion says *link*; the recommendation is *code*, and this is the paragraph to argue with. **Does it satisfy the second factor?** This is the question that decides whether the feature is safe or a hole. If somebody with TOTP configured can sign in by a code from their inbox and skip the second factor, the feature has removed their second factor by adding a first one. allauth applies MFA after authentication, so the answer is probably *no bypass* — but "probably" is not the standard here. It needs a test that says so explicitly, in `tests/security/`, of the same kind that already covers the other boundaries. **And whether it counts *as* a factor is a separate question with an existing answer.** `sso_is_second_factor` shows the shape: a nullable boolean on `SiteSettings`, a help text that says what it means, and no default that quietly decides for the operator. This wants the same, and probably wants to say no. **It makes email more load-bearing, not less.** Today email resets a password; with this it signs somebody in. Anybody who holds the mailbox holds the account, without needing to complete a reset. That is not an argument against it — it is already true of password reset — but it is an argument for the instance being honest about it on the settings page, and for #104's lock reasoning getting stronger rather than weaker. **"Configured and working" is #152 and cannot be assumed.** `recovery_routes()` counts a transport that is *selected*, not one that *delivers*. Offering sign-in by email on an instance whose SMTP is misconfigured produces a sign-in page that promises something it cannot do, to somebody who may have no other way in. The switch should be offered only where mail is known to work, which is exactly what that issue is for. **The primary address, and only a verified one.** Allauth allows up to five addresses; a code goes to the primary. If the primary changes, the route changes with it, silently — worth saying on the addresses page rather than leaving somebody to discover it. **Enumeration.** The response to "send me a code" must not differ between an address that exists and one that does not, and the message must say so in a way that is honest rather than evasive — the same care #90's uniqueness message took. **Rate limits and resend.** `LOGIN_BY_CODE_MAX_ATTEMPTS` and `LOGIN_BY_CODE_MAX_RESEND_COUNT` exist; the numbers are a decision, and the failure has to be a refusal that says when to try again, like the 429 with `Retry-After` that #112 established. **One more route to explain on a page that already explains several.** *Settings → Account* already says how sign-in works, with passkeys, TOTP and SSO each described. A fourth way in needs its sentence there, and the strings are 39 translations.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 11:09:36 +00:00
Author
Owner

A code, as the issue recommended and against what the request said. The paragraph
asking to be argued with was right: a link is a bearer credential that works from anywhere,
corporate mail scanners follow links, and Postulo's people correspond with recruiters whose
mail has exactly those scanners. ACCOUNT_LOGIN_BY_CODE_ENABLED, and the reasoning is in the
settings file where the next person to wonder will find it.

Does it satisfy the second factor? No — asserted, not assumed.
tests/security/test_email_sign_in.py signs somebody with an authenticator through the whole
flow and checks they are not in the session, with a companion test doing the same for
somebody without one and checking they are. Without the second test the first would pass on
a broken path.

Whether it counts as a factor: no, and deliberately no setting. This is where I departed
from the issue, which suggested following sso_is_second_factor's shape. The asymmetry is
the reason: that setting exists because an identity provider may itself have checked identity
carefully and Postulo cannot see how — so there is a real judgement for an operator to make. A
code out of an inbox has no provider behind it to trust. There is nothing to decide, and a
switch that should always be off is a switch that will one day be on. A test asserts no such
field exists, so adding one is a deliberate act. Worth pushing back on if you disagree.

"Configured and working" is honoured through #152. site.email_sign_in() is the
administrator's yes and mail actually delivering, and both the switch and the page read it;
the door closes again by itself if the relay starts failing.

Enumeration — one test asserts the response to a known and an unknown address is the same
page with the same status.

Rate limits and resend — three minutes, three attempts, three resends, and
LOGIN_BY_CODE_TRUST_ENABLED = False because a one-off code must not become a standing
credential on a machine that may not be theirs next week. All three are environment variables.

The primary address changing is now said on Settings → Account beside the addresses,
rather than left to be discovered, and the fourth way in has its sentence there.

One implementation note. allauth decides whether to offer a code from a Django setting read at
import, because that is when it builds its URLs — so a database switch cannot add or remove
the route. The door is therefore always built, and Postulo shadows both paths with its own:
a LoginView subclass that corrects one fact in the context, and a guard on login/code/
that 404s where the instance does not offer it. A subclass rather than a copy of their sign-in
template, or every allauth release becomes a merge here.

14 tests in tests/security/, both new pages walked by the browser suite under axe-core, a
wiki section, and five strings in all 39 European catalogues.

Shipped in 810458d on 0.3.0, with main kept level.

**A code, as the issue recommended and against what the request said.** The paragraph asking to be argued with was right: a link is a bearer credential that works from anywhere, corporate mail scanners follow links, and Postulo's people correspond with recruiters whose mail has exactly those scanners. `ACCOUNT_LOGIN_BY_CODE_ENABLED`, and the reasoning is in the settings file where the next person to wonder will find it. **Does it satisfy the second factor? No — asserted, not assumed.** `tests/security/test_email_sign_in.py` signs somebody with an authenticator through the whole flow and checks they are *not* in the session, with a companion test doing the same for somebody without one and checking they *are*. Without the second test the first would pass on a broken path. **Whether it counts *as* a factor: no, and deliberately no setting.** This is where I departed from the issue, which suggested following `sso_is_second_factor`'s shape. The asymmetry is the reason: that setting exists because an identity provider may itself have checked identity carefully and Postulo cannot see how — so there is a real judgement for an operator to make. A code out of an inbox has no provider behind it to trust. There is nothing to decide, and a switch that should always be off is a switch that will one day be on. A test asserts no such field exists, so adding one is a deliberate act. **Worth pushing back on if you disagree.** **"Configured and working" is honoured through #152.** `site.email_sign_in()` is the administrator's yes *and* mail actually delivering, and both the switch and the page read it; the door closes again by itself if the relay starts failing. **Enumeration** — one test asserts the response to a known and an unknown address is the same page with the same status. **Rate limits and resend** — three minutes, three attempts, three resends, and `LOGIN_BY_CODE_TRUST_ENABLED = False` because a one-off code must not become a standing credential on a machine that may not be theirs next week. All three are environment variables. **The primary address changing** is now said on *Settings → Account* beside the addresses, rather than left to be discovered, and the fourth way in has its sentence there. One implementation note. allauth decides whether to offer a code from a Django setting read at *import*, because that is when it builds its URLs — so a database switch cannot add or remove the route. The door is therefore always built, and Postulo shadows both paths with its own: a `LoginView` subclass that corrects one fact in the context, and a guard on `login/code/` that 404s where the instance does not offer it. A subclass rather than a copy of their sign-in template, or every allauth release becomes a merge here. 14 tests in `tests/security/`, both new pages walked by the browser suite under axe-core, a wiki section, and five strings in all 39 European catalogues. Shipped in `810458d` on `0.3.0`, with `main` kept level.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#153
No description provided.