A second login carrying no email claim collides on the unique index and 500s #150
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: LOW — not reachable against the current provider, but it is a 500 where there used to be a clean refusal
Found while implementing #149, and deliberately not fixed there: every available fix is a product decision that issue had not taken.
The bug
User.emailisunique=True(backend/app/models/models.py:294), and the callback stores(userinfo.get("email") or "").lower()— so an identity with noemailclaim is written with the empty string.Before #149,
is_email_allowed("")returnedFalseand such a login was refused at the gate, before any row was written. That gate is gone. Now the first email-less identity is created withemail = "", and the second collides on the unique index →IntegrityError→ 500.Not a privilege problem. Accounts are keyed on
provider_sub, so no takeover is possible and no session is issued — the write simply fails. It is a crash where a refusal belongs.Not currently reachable
Authentik binds the
emailscope to the Circa provider and every account on that instance is admin-created with an address. So this needs either a provider that omits the claim, a scope binding removed, or a second provider added later.Recording it rather than fixing it blind, because it becomes reachable the moment any of those changes and the symptom — a 500 on the second such person's first login — points nowhere near the cause.
The options, none of which is obviously right
emailnullable and drop it from the unique index, or make the index partial onemail IS NOT NULL. Most honest about the data: an address the provider did not supply is genuinely unknown, andNULLsays so where""does not. Costs a migration, and every consumer ofuser.emailneeds checking for the None case — the display path in particular (#107 renders display names, and the fallback chain ends at the address).provider_sub. Keeps the column non-null and unique with no migration, and is the cheapest. But it invents a fact — the UI would show something that looks like an address and is not one, which is the #78 shape this project keeps refusing elsewhere.My inclination is (2): it is the only one that records what is actually known. But it is a migration and a sweep of every
user.emailreader, so it wants doing deliberately rather than folded into an unrelated change.Worth doing at the same time
UserRepository.get_by_emailhas zero call sites inapp/ortests/— verified while implementing #149. It is the one piece of code that could quietly make the address load-bearing again, so it should go, or gain a comment saying why it exists.References
backend/app/models/models.py:294— the unique constraintbackend/app/api/routes/auth.py— the callback's upsert