Trusted-Device 2FA in ASP.NET Core
Two-factor authentication is a tax charged on every login. Implementing it is the easy part. The interesting question is how to charge it less often without quietly pretending you have added security.
- Role
- Backend developer
- Where
- Vertex Special Technologies
- When
- 2025
The actual problem
Turning on two-factor authentication is a few lines against the framework's identity system. An authenticator app scans a QR code, or a code goes out by email, and the login verifies it. That part is solved and not worth writing about.
What is worth writing about is what happens next. An internal tool that people open several times a day now demands a six-digit code several times a day, from the same laptop, at the same desk. The security team is happy and everybody else starts asking whether 2FA can be turned off.
So the real requirement was: ask for the second factor when the situation is new, and stay quiet when it is not.
What “this device” means over HTTP
A server has no reliable notion of a device. It has a request. So a fingerprint is assembled from what the request carries, hashed, and stored against the user.
The user-agent string is parsed into structured fields rather than used raw, so a browser's patch-version bump does not invalidate the match while a change of browser or operating system does. The whole set is concatenated and hashed, and that hash is what gets stored.
What is stored is a hash, a human-readable device name, and the address it was last seen from. Not a profile. A user can look at their trusted devices, recognise them by name, and remove any of them.
When the question gets asked
- 1Credentials are checked as normal. If two-factor is off, nothing else happens.
- 2If it is on, the request is fingerprinted and looked up against that user's trusted devices.
- 3Known and used within three months — the login proceeds, and the device's timestamp is refreshed so the window slides forward.
- 4Otherwise the second factor is requested, by authenticator app or email.
- 5On a correct code the device is stored, named, and trusted from then on.
Three months, measured from last use
The window came from the requirement. What was left to decide was whether it ran from enrolment or from last use, and last use is the kinder reading: a laptop opened every morning stays trusted, one left in a drawer goes quiet on its own.
Enabling 2FA trusts the device you enable it on
Otherwise turning on the feature immediately challenges you on the machine you just proved yourself on, which reads as the feature being broken on first contact.
What this is, and what it is not
Every input to that fingerprint is either sent by the client or derived from the network. None of it is a secret, and nothing about it proves identity.
This matters more than any implementation detail on the page. Someone who can replay the same headers from the same network produces the same hash and skips the challenge. A device fingerprint is not a second factor and must not be counted as one.
What it is: a decision about how often to ask. The password is still required every time. The second factor is still required whenever the situation is unfamiliar. What the fingerprint buys is silence in the common case, and the honest cost is a window in which a sufficiently determined imitation of a known device is not challenged.
For an internal tool behind a login, on a three-month sliding window, with the device list visible and revocable by the user, that trade is worth making. For a banking application it would not be. The design is only defensible because of where it was deployed, which is the part usually left out.
The requirement, and what it cost
The IP address is part of the hash. That was not an accident of implementation: the rule handed down was that a change of network counts as a change of situation, and a change of situation gets re-verified. Signing in from the office and signing in from a café are not the same event, and the second one should be challenged.
It is a defensible rule. It also has a consequence that is obvious in hindsight and was obvious to users almost immediately.
What the rule buys
Headers are trivial to copy. Someone who has seen a user's User-Agent still cannot match the fingerprint from their own network. It raises the floor from “copy a string” to “copy a string and be on the same network”, and it makes an unfamiliar location a thing the system reacts to rather than ignores.
What it costs
An address is not a property of a device. A phone moving from office wifi to mobile data is a new situation. So is a laptop after the router reboots. People noticed, and said so. The users challenged most often were the ones who move around, which is the group the feature was supposed to help.
The interesting part is that the complaints were not evidence the rule was wrong. The rule was doing precisely what it had been asked to do. A requirement that never inconveniences anyone is usually a requirement that is not being enforced.
What was actually missing was an explanation. A challenge that arrives with no reason reads as the system being broken. The same challenge, labelled as an unrecognised network, reads as the system working, and the people complaining stop being confused users and become users who understand the trade being made on their behalf. That is a copy problem sitting inside what looked like an architecture problem, and it took the complaints to see it.
What I would do differently
Separate “same device” from “same network”
The fingerprint answered both questions with one hash, which is why they could not be tuned apart. A signed, http-only cookie issued on a successful challenge would answer the first properly: something the server minted rather than inferred, and not reproducible by copying headers. The network rule then stays exactly as required, as a deliberate separate trigger rather than a side effect of the identity check.
Say why the code is being asked for
The single highest-value change, and it is text. An email already goes out when two-factor is enabled or disabled; nothing tells a user why they are being challenged right now, or that a new device has just been trusted on their account. Both are moments a person would want to know about, and one of them is the moment a stranger would create.