KapfenbergWhat happens if …?Security and accessLocal over cloud

Blink answers with 202 — and nobody got in

The login failed in the version I tested. Three days later the project had three new releases. What lay in between was not code.

In part one I took a project for half asleep and was wrong. The actual finding was not “nobody is interested” but “nobody gets in”.

Here is what caused it. It was a three-digit number nobody checked for.

The documented route did not work

Logging in to Blink requires a PIN by SMS. The add-on provided for two starts: the first begins the login and the PIN arrives, the second is where you enter it.

That never worked. Every attempt produced the SMS, and every attempt was followed by Login failed.

My first find was self-inflicted: the options file had Windows line endings, so the start script read one character too many. An embarrassing mistake, quickly fixed — and afterwards the same Login failed stood there again.

The finding that looked final

Read in the source: the login stores two values in memory at the first step that the second step needs. Restart the service and they are gone.

if not hasattr(self, "_oauth_csrf_token") or not hasattr(self, "_oauth_code_verifier"):
    _LOGGER.error("No OAuth 2FA state found. Start login flow first.")
    return False

That makes the two-start login impossible in principle. Not “does not work right now”, but: it cannot succeed while a restart sits between the two steps.

I took that for the final finding. It was only half the answer.

The observation that overturned it

Then came the sentence that changed direction — and it came not from the source but from the house:

On the second attempt Blink sent the same PIN as on the first.

That is not a side note. The PIN does not expire — the session does. The PIN belongs to the account and stays valid; what is missing are the two values from memory.

And from that the simplest route follows immediately: if the PIN is known in advance, nobody has to wait for a second start.

login -> 2FA demanded -> PIN ready -> redeemed at once -> token saved

No waiting block, no second start. The waiting logic remains as a fallback for cases where the PIN is not known in advance.

An everyday observation beat a code analysis here. I had read the source and understood it correctly — and still drawn the wrong conclusion, because I did not know the PIN stays the same.

And then the number

With an extra diagnostic line in the log the missing fact appeared:

oauth_signin HTTP 202
Login failed

The library in use knows exactly two cases at this point:

if   response.status == 412:                      return "2FA_REQUIRED"
elif response.status in [301,302,303,307,308]:    return "SUCCESS"
return None                                       # -> "Login failed"

But Blink answers with 202 Accepted — credentials accepted, 2FA code sent. That case falls through both checks and is reported as a failure.

Everything is explained in retrospect:

Observation Explanation
every attempt produced an SMS the login succeeded every time
yet Login failed appeared every time 202 is not recognised
three times the same PIN the same open challenge, nothing was locked

The login had never failed. It was simply read wrongly.

An error message saying “Login failed” when the login succeeded sends every user in the wrong direction. You check passwords, accounts, lockouts — and none of it is the problem.

That is why nobody got in. Not lack of interest, but a status check that does not know about a third case.

What happened next

I submitted two contributions. What happened over the following 72 hours finally disposed of my assessment from part one:

30.08. two contributions submitted
31.08. maintainer merges eight contributions, v0.2.0, automated build checks, roadmap
31.08. one of mine merged “taken as written”
01.09. detailed review of the second, three concrete points
02.09. a second, independent tester tries four cameras from my branch
02.09. v0.3.0, v0.4.0, v0.4.1 — nine commits in one day

Three version jumps and a second independent tester in three days.

What the nudge consisted of — not volume of code

This is the part I consider most important. My share was smaller than the effect suggests. It was not a large contribution and not a new feature.

What counted was naming two blocking faults precisely, with the reason:

  • The login failed in the version I tested — and why: HTTP 202 instead of 412, and the session-bound values.
  • The video stream aborts before the camera is even woken — and why: because Blink’s server answers differently in three places from what the protocol description leads one to expect.

Two diagnoses, each with a log excerpt and a source location. Not “does not work for me”, but “here is the line, and here is what comes back instead”.

So what?

  1. A wrong error message costs more than none. Login failed on a successful login probably drove more people away than any missing feature.
  2. Read the source — but do not trust your conclusion immediately. I had read everything correctly and was still on the wrong track until an everyday observation arrived.
  3. An observation beats an analysis when it shows something the code does not say. That the PIN stays the same is written nowhere. You only see it by looking twice.
  4. A precise bug report is a contribution. You do not have to program anything to move a project forward. You have to be able to say exactly where it sticks.
  5. Check the return values that are not handled. Two cases checked, everything else an error — that is a trap sitting in a great deal of code.

The third part gets uncomfortable: the maintainer replies at length — and corrects me. Rightly.