One Door for Every Way In
A product with three ways to sign in has three places to forget the two-factor challenge. This one has three ways in and one place where signing in actually happens, and the difference is a rule that can be checked by reading rather than by testing.
Adding single sign-on to an existing product is where a lot of security controls quietly stop applying. Not through carelessness — through arithmetic. There was one sign-in path with a two-factor challenge on it. Now there are two, and the second was written by somebody implementing an identity provider handshake, for whom the challenge is a different feature in a different file.
Three ways in, one place where it happens
This product accepts three kinds of proof: a password, a single-use link sent to the address on file, and a provider-verified identity from a sign-on provider.
They establish who you are in completely different ways and they converge on one function. That function signs you in and runs everything that has to happen afterwards, in order, every time.
Proving who you are and being allowed in are different questions. Three answers to the first; one answer to the second.
The instruction beside it is unusually blunt for a codebase, and it is the point: if you add an authentication route, call this — do not sign somebody in and send them to the dashboard yourself.
That sentence exists because the alternative is so natural. A developer wiring up a new provider has a verified user in hand and a redirect to write. Signing them in is one line. Nothing about that line looks like it is skipping five controls, and nothing will report that it did.
What the chain actually checks
-
Is the account deactivated?
Somebody who has left is refused whichever way they arrive. A provider that still recognises them is not the authority on whether they still work here.
-
Is the email address verified?
Checked regardless of method, because an unverified address is an unproven claim about who owns the account.
-
Has an administrator forced a password change?
A control that would otherwise be trivially avoidable by signing in with a link instead.
-
Has the plan expired?
A commercial gate, in the chain because it is one more thing that would apply on one route and not another.
-
And the two-factor challenge
Last, and the reason the chain exists. Somebody with a second factor enrolled is challenged whichever way they proved who they are.
The order matters. A deactivated account is refused before anything asks it for a second factor, so somebody who has left is not prompted for a code. A forced password change is imposed before a challenge, so the sequence is the one an administrator intended.
None of that ordering is obvious from any individual gate. It is a property of them being in one sequence in one place — and it would be a different, unexamined order in each of three implementations.
Why the second factor is the one that matters
Because of what a sign-on provider actually proves.
A verified identity from a provider proves the person controls that account with that provider. It does not prove they hold your second factor, and the two are frequently the same thing to an attacker: somebody with a compromised work email account has the provider identity.
A product where the password route challenges for a second factor and the sign-on route does not has published a bypass. It is not a weakened control; it is an alternative entrance with no control on it at all, and it is precisely the entrance a compromised email account walks through.
The chain means enrolment in a second factor covers every route by construction. Not because each was remembered, but because there is only one place where being allowed in happens.
What passwordless changes, and what it does not
The chain knows which method was used, and it is used to relax exactly one thing rather than several.
Where the identity was proven by possession — a provider-verified address, or a link delivered to the address on file — the caller has already demonstrated control of that address. So the checks specific to proving an address are not re-run against somebody who just proved it.
What does not change is everything else. A deactivated account is still refused. A second factor is still required. The relaxation is narrow, named, and passed as an explicit argument rather than inferred — so a route claiming a relaxation has to say so, in a way visible in review.
The line most people forget
One statement in the middle of that function is worth its own paragraph.
Immediately after signing somebody in, the session is regenerated — the identifier is replaced with a new one.
That closes an attack most people have heard of and few can describe. If somebody can cause a victim's browser to hold a session identifier of the attacker's choosing before they sign in — through a link, a subdomain, an injected value — then after signing in, that identifier is a signed-in session the attacker also holds.
Replacing the identifier at the moment of sign-in makes any previously known one worthless. It is one line, it has no visible effect, and it is another beneficiary of there being one place where signing in happens — because it is a line that gets written once and forgotten twice.
What is in place, layer by layer
Every route in, one chain
One place where signing in happens
Password, provider identity and single-use link all converge on the same function, so a new route cannot quietly skip a control.
A written instruction beside it
The rule that authentication routes must call it — rather than signing somebody in directly — is recorded where the next person to add a route will read it.
Deactivated accounts refused first
Ahead of every other gate, so somebody who has left is not challenged for a second factor before being turned away.
Address verification checked on every route
Regardless of how the person arrived, because an unverified address is an unproven claim about who owns the account.
Administrator-forced changes honoured
A forced password change applies whichever way somebody signs in, rather than being avoidable by choosing a different route.
The second factor last, and always
Enrolment covers every entry point by construction, so a provider identity is not an entrance with no challenge on it.
A narrow, explicit relaxation
Passwordless methods relax only the checks specific to proving an address, and the route has to declare it rather than have it inferred.
The session identifier replaced
Regenerated at the moment of sign-in, so any identifier known beforehand is worthless afterwards.
The intended destination preserved
Somebody sent to sign in from a deep link arrives back where they were going, once every gate has been passed.
The gates run in a fixed order, and the order is a property of them being in one sequence. Three separate implementations would each have their own order, and none of them would have been decided.
Three positions held on purpose
- There is one place where being allowed in happens, and adding an entry point means using it. A rule that can be checked by reading is worth more than a set of controls that have to be re-verified on every route.
- The second factor applies to every method, including the ones where the provider has already verified an identity. A provider proves control of that provider account, which is exactly what a compromised work mailbox gives an attacker.
- The relaxation for passwordless methods is narrow and declared. A route claiming it has to say so, so it is visible in review rather than inferred from the absence of a password.
Five questions about single sign-on
Does two-factor apply to SSO sign-ins?
A good answer sounds like
Yes, always.
What ours actually is
Every method converges on one chain, and the challenge is part of it.
Can a deactivated user sign in with SSO?
A good answer sounds like
No.
What ours actually is
Refused at the first gate, before anything else runs.
What if you add a new sign-in method?
A good answer sounds like
It uses the same chain.
What ours actually is
The rule is written beside the function, and a route that ignores it is visible in review.
Is the session identifier changed at sign-in?
A good answer sounds like
Yes.
What ours actually is
Regenerated immediately, so anything known beforehand is worthless afterwards.
Do passwordless routes skip anything?
A good answer sounds like
One narrow thing.
What ours actually is
Only the checks specific to proving an address, declared explicitly by the route.
Our take
Every authentication control is only as good as the number of routes it is on, and that number goes down whenever somebody adds a feature. It is never a decision — nobody proposes an entrance without a challenge on it. It happens because signing a verified user in is one line, and that line looks complete. The only durable defence is architectural: make there be one place where being allowed in happens, put the rule in writing next to it, and make any route that bypasses it obvious to anybody reading. Then the question "does two-factor apply to this new method" is answerable by reading rather than by testing, and it stays answerable as the product grows.
Ask which sign-in routes your controls are on
Not whether two-factor is enabled — whether it is on every entrance. The answer is often one fewer than everybody assumes.
Talk through sign-in securityFrequently asked questions
Does two-factor authentication apply when signing in with Google?
Yes. Every sign-in method converges on one gate chain and the challenge is part of it, so enrolling a second factor covers all routes by construction rather than because each was remembered separately.
Can somebody whose account was deactivated still sign in through Google?
No. The deactivation check runs first, before anything else — including before any second-factor prompt, so somebody who has left is turned away rather than challenged. A provider still recognising the account is not the authority on whether they still work with you.
What happens if an administrator forces a password change?
It applies whichever way the person signs in. Without a shared chain, that control would be avoidable simply by choosing a passwordless route — which is exactly the kind of gap that appears when each entry point implements its own sequence.
Is anything skipped for passwordless sign-in?
Only the checks specific to proving control of an email address, because a provider-verified identity or a single-use link to the address on file has already proved it. Everything else — deactivation, forced changes, the second factor — applies identically, and a route claiming that relaxation has to declare it.
How do you make sure a new sign-in method does not bypass the controls?
By having one function where signing in happens, with the rule written beside it. A route that calls the framework directly and redirects is visible in review as not using it — which is a much more reliable check than testing each control against each route after the fact.