---
title: 'Authentication Implementation Errors'
source: 'https://academia.sh/en/courses/secure-coding/authentication-implementation-errors'
course: 'Secure Coding'
language: en
updated: '2026-09-26T03:02:39+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts the channels that leak user existence in a point inventory'
    - 'Measures the response-time difference with a band check, without writing the raw duration'
    - 'Compares the coverage of a single-channel countermeasure against a central validator'
---

# Authentication Implementation Errors

Measuring the implementation error in terms of leaked information: at eight authentication points, a distinct error message, work skipped when no record exists, and a comparison that exits on the first mismatch are counted as three channels, and five points leak; the response-time difference is shown through a band check rather than by writing the raw duration; a countermeasure that closes only one channel closes just one of the five; a central validator that closes all three channels automatically covers the ninth point added; and when a fourth channel is included in the count, the same pattern's number of uncovered points rises to two.

The previous topic drew the boundary of data coming from outside and wrote the same three
columns for every point set: how many points there are, how many the pattern covers, and how
many points are left uncovered. All of those measurements rested on a single assumption — that
the identity of the person behind the request is already known. Data can conform to the rules
and still have come from the wrong person.

This topic removes that assumption. Which flow authentication is done with is not addressed
here; the types of flows and how credentials are stored are the subject of the Authentication
and Authorization course. What is measured here is the **implementation**: does the response
the server gives to someone who does not know the correct credential at all say something even
without knowing the correct information?

## Leaked Information Can Be Counted

An authentication point knows two things separately: whether the requested record exists, and
whether the submitted credential is correct. The response should only state the second. The
first being distinguishable from the outside is called **user enumeration**: an attacker learns
which subscriber numbers are registered without knowing any password at all. This information
does not grant access by itself; it narrows the target set for every subsequent attempt.

Attempting enumeration against a service **is both illegal and unprofessional without written
authorization and a defined scope**. The inventory and measurement below run on the fictional
regional measurement network's own code; there is no real service, no real account, and no real
password.

```js
// identity/point.mjs — the subscriber portal's authentication points (model): no real
// service, no real account and no real password. No randomness, the amount of work is fixed.
import { createHash } from "node:crypto";

// IE1: a point can leak user existence through three separate channels — is the error
// message distinct, is the work done different when no record exists, does the comparison
// exit on the first mismatch. The fourth field `lock` is a channel not counted in the
// inventory; it is added to the count in the last section of the lesson.
export const POINT = [
  { name: "portal-login",          message: "single",   work: "fixed",        compare: "fixed",       lock: "distinct" },
  { name: "portal-reset",          message: "distinct", work: "early-return", compare: "fixed",       lock: "single" },
  { name: "portal-email-change",   message: "distinct", work: "fixed",        compare: "fixed",       lock: "single" },
  { name: "field-app-login",       message: "single",   work: "fixed",        compare: "fixed",       lock: "single" },
  { name: "meter-reading-key",     message: "single",   work: "early-return", compare: "early-exit",  lock: "single" },
  { name: "invoice-query",         message: "distinct", work: "early-return", compare: "early-exit",  lock: "single" },
  { name: "second-factor-code",    message: "single",   work: "fixed",        compare: "fixed",       lock: "distinct" },
  { name: "report-endpoint-key",   message: "single",   work: "fixed",        compare: "early-exit",  lock: "single" },
];

// IE2: the unit of work is a fixed-step hash loop; the raw duration is not written, the step
// count is.
export const SEARCH = 2000, VERIFY = 20000;
const loop = (seed, step) => {
  let s = seed;
  for (let i = 0; i < step; i++) s = createHash("sha256").update(s).digest("hex");
  return s;
};
const RECORD = new Set(["a-4471", "a-5120", "a-6033"]);

// Flawed implementation: when no record exists, the verification work is never done, and the
// message diverges too.
export function flawed(subscriber, submitted) {
  loop(subscriber, SEARCH);
  if (!RECORD.has(subscriber)) return { message: "subscriber not found", step: SEARCH };
  loop(submitted, VERIFY);
  return { message: "incorrect password", step: SEARCH + VERIFY };
}

// Patterned implementation: the same work is done even when no record exists, the message is
// single.
export function patterned(subscriber, submitted) {
  loop(subscriber, SEARCH);
  const found = RECORD.has(subscriber);
  loop(found ? submitted : "placeholder-comparison-seed", VERIFY);
  return { message: "incorrect subscriber number or password", step: SEARCH + VERIFY };
}
```

```js
// identity/coverage.mjs — how many points there are, how many the pattern covers, how many
// points are left uncovered.
import { POINT, flawed, patterned } from "./point.mjs";
const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

// IE3: a point is considered to leak user existence if at least one of the three channels is
// open.
const CHANNELS = ["message", "work", "compare"];
const channel = (n, closed = [], list = CHANNELS) => list.filter((k) =>
  !closed.includes(k) && n[k] !== "single" && n[k] !== "fixed");
const A = [-24, 9, 14, 15, 10, 12];
console.log(`1. ${POINT.length} authentication points and their open channels`);
write(A, "point", "message", "no record", "compare", "channels", "leaking");
for (const n of POINT)
  write(A, n.name, n.message, n.work, n.compare, channel(n).length, channel(n).length ? "YES" : "no");
const leaking = (set, closed, list) => set.filter((n) => channel(n, closed, list).length).length;
console.log(`leaking point: ${leaking(POINT, [])} / ${POINT.length}`);

// IE4: duration is not written raw. The expected ratio is derived from the step count; the
// measured value only passes through a band check and yes/no is written.
const median = (d) => d.slice().sort((a, b) => a - b)[d.length >> 1];
const measure = (f, subscriber) => {
  const d = [];
  for (let i = 0; i < 15; i++) {
    const t = process.hrtime.bigint();
    f(subscriber, "submitted-value");
    d.push(Number(process.hrtime.bigint() - t));
  }
  return median(d);
};
const B = [-16, 11, 12, 16, 13, 9, 17];
console.log("\n2. response-time difference (the raw duration is not written; the expected ratio comes from the step count)");
write(B, "implementation", "no record", "has record", "expected ratio", "band", "in band", "existence leaks");
for (const [name, f] of [["flawed", flawed], ["patterned", patterned]]) {
  const none = f("a-0000", "x").step, has = f("a-4471", "x").step;
  const expected = has / none, low = expected / 2, high = expected * 2;
  const measured = measure(f, "a-4471") / measure(f, "a-0000");
  write(B, name, none, has, expected.toFixed(2), `${low.toFixed(2)}..${high.toFixed(2)}`,
    measured >= low && measured <= high ? "yes" : "NO", expected > 1.5 ? "YES" : "no");
}

// 3. coverage: how many points a countermeasure closes on its own
const NEW_POINT = { name: "field-device-pairing", message: "distinct", work: "early-return", compare: "early-exit" };
const extended = [...POINT, NEW_POINT];
const C = [-44, 7, 11, 12];
console.log("\n3. how many points a countermeasure closes");
write(C, "countermeasure", "points", "leaking", "uncovered %");
for (const [name, k, closed] of [
  ["today's distributed state", POINT, []],
  ["constant-time comparison only", POINT, ["compare"]],
  ["single error message only", POINT, ["message"]],
  ["central validator (three channels)", POINT, ["message", "work", "compare"]],
  ["new point added, distributed", extended, []],
  ["new point added, central pattern", extended, ["message", "work", "compare"]],
]) write(C, name, k.length, leaking(k, closed), ((leaking(k, closed) / k.length) * 100).toFixed(1));

// 4. if the inventory itself is incomplete: the lock response is included in the count as a
// fourth channel
const FOUR = [...CHANNELS, "lock"];
console.log("\n4. once a fourth channel is included in the count");
write(C, "count", "points", "leaking", "uncovered %");
for (const [name, list, closed] of [
  ["three channels, central validator", CHANNELS, CHANNELS],
  ["four channels, central validator", FOUR, CHANNELS],
  ["four channels, lock response also uniform", FOUR, FOUR],
]) write(C, name, POINT.length, leaking(POINT, closed, list),
  ((leaking(POINT, closed, list) / POINT.length) * 100).toFixed(1));
```

```
1. 8 authentication points and their open channels
point                     message     no record        compare  channels     leaking
portal-login               single         fixed          fixed         0          no
portal-reset             distinct  early-return          fixed         2         YES
portal-email-change      distinct         fixed          fixed         1         YES
field-app-login            single         fixed          fixed         0          no
meter-reading-key          single  early-return     early-exit         2         YES
invoice-query            distinct  early-return     early-exit         3         YES
second-factor-code         single         fixed          fixed         0          no
report-endpoint-key        single         fixed     early-exit         1         YES
leaking point: 5 / 8

2. response-time difference (the raw duration is not written; the expected ratio comes from the step count)
implementation    no record  has record  expected ratio         band  in band  existence leaks
flawed                 2000       22000           11.00  5.50..22.00      yes              YES
patterned             22000       22000            1.00   0.50..2.00      yes               no

3. how many points a countermeasure closes
countermeasure                               points    leaking uncovered %
today's distributed state                         8          5        62.5
constant-time comparison only                     8          4        50.0
single error message only                         8          4        50.0
central validator (three channels)                8          0         0.0
new point added, distributed                      9          6        66.7
new point added, central pattern                  9          0         0.0

4. once a fourth channel is included in the count
count                                        points    leaking uncovered %
three channels, central validator                 8          0         0.0
four channels, central validator                  8          2        25.0
four channels, lock response also uniform         8          0         0.0
```

The numbers belong to the **measurement** class; their inputs are IE1–IE4.

## Five Points and Three Channels

The first table opens the inventory. **Five** of the eight points leak user existence, and how
many channels the leak comes from varies from point to point.

`portal-email-change` leaks only through the message: when no record exists it returns a
different text, while the amount of work stays the same. `report-endpoint-key` is the opposite;
its message is single and it does the same work when no record exists, but the key comparison
exits as soon as it reaches the first mismatched byte. `invoice-query` keeps all three channels
open at once.

This distribution shows why the inventory is derived point by point. The sentence "error
messages are single" is a coverage claim, and it has a counterpart in the table: five points
whose message column reads `single`. But two of those five points continue to leak through
another channel. The channel count does not say how open a point is; it says **how many
separate places need a fix**.

It is worth noting that not every point in the inventory is a login screen. `meter-reading-key`
and `report-endpoint-key` are endpoints no human sees; the credential is a key, authentication
still happens, and the same three channels are still open. If a point is counted as an
authentication point only when a "user login" is found, the inventory comes out incomplete. The
criterion is a single sentence: if a submitted value at a point is compared against a known
record, that point belongs in the inventory.

## Duration Is Not Written, the Band Is Checked

The second table is this lesson's measurement pattern. The response-time difference is a real
leak channel, but a raw millisecond is not a measurement: it depends on the machine, the load,
and the workload at that moment, it gives two different numbers across two runs of the same
code, and whether the two numbers are comparable at all is unknown.

The measurement is set up as follows. The expected value **is derived from the input**: the
flawed implementation does 2,000 steps when no record exists and 22,000 steps when a record
exists; the expected ratio is 11.00. The band lies between half and twice that value. The
measured duration ratio is reported only by whether it falls within this band — the number
itself is not written to the output, only `yes` is. In the patterned implementation, both paths
take 22,000 steps, the expected ratio is 1.00, and the measured value falls in the 0.50–2.00
band.

The last column makes the call. If the expected ratio is close to 1, there is no information to
be drawn from the time difference; if it is 11, there is. This distinction does not change from
run to run, because it rests on the step count, not the raw duration. The measured value's only
job is to confirm that the model actually behaves this way.

**Constant-time comparison** closes only one part of this channel: the early exit inside the
comparison itself. The verification work not being done at all when no record is found comes
before that point, and constant-time comparison does not touch it. The 11.00 ratio in the
second table stays at 11.00 with constant-time comparison.

## What a Single-Channel Countermeasure Closes

The third table counts coverage. In today's distributed state, five of the eight points leak;
the uncovered rate is **62.5%**.

If only constant-time comparison is applied, the number of leaking points drops from 5 to
**4**. The single point it closes is `report-endpoint-key`, because its only open channel was
the comparison. Applying only a single error message gives the same result: 5 to 4. The two
countermeasures close different points, but both have the same coverage. A control that
measures a leak class through only one of the three channels leaves four-fifths of the job
open.

The **central validator**, which closes all three channels together, brings the number of
leaking points to zero. The pattern's content is a single sentence: the same work is done
whether or not the record is found, the same message is returned, and the comparison is
constant-time. This pattern is not a list of rules but a single function; the points call it.

The difference is where the rule is written. In distributed defense, the rule "do not let error
messages diverge" has to be applied in eight separate places, and whether it was applied
correctly can only be known by reading all eight places one by one. In the central pattern, the
same rule is applied in one place; there is one place to read and one place to test. This is
also what the coverage count means: 8 out of 8 coverage is not eight correct implementations,
it is **one** correct implementation and eight calls.

The last two rows answer this course's third question. When a ninth point —
`field-device-pairing` — is added to the inventory, the number of leaking points in the
distributed state rises from 5 to 6 and the uncovered rate climbs to 66.7%. In the central
pattern, the same point is covered with no extra work, and the number of leaking points stays
at 0. The difference is not in the pattern's strength but in **whether the new point has to
call the defense**: in distributed defense every new point is a candidate for a hole, in the
central pattern it is only a call.

## The Inventory Itself Can Be Incomplete

The last row of the third table reads zero, and this number is misleading. Zero does not say
"no point leaks"; it says **no point leaks through the three channels being counted**. The
coverage measure depends on the set of channels being counted.

The fourth table adds a fourth channel to the inventory: the response reporting that the
account has been locked after successive failed attempts. This response says that a lockable
account **exists**; a subscriber number that is not registered is never locked. In the
three-channel count, the central validator scored zero; in the four-channel count, the same
validator leaks **at 2 points** — an uncovered rate of 25%. The lock decision is made not in
the validator itself but in the counter layer that comes after it; because the pattern does not
call that layer, it does not touch it.

The last row shows how the pattern is extended: once the lock response is also pulled into a
uniform shape, the count resets to zero. The lesson here is not the pattern's inadequacy but
**the limit of the measure**. The coverage percentage does not state a defense's strength; it
states its position relative to the number of channels counted in the inventory. When a new
channel is discovered, every past coverage count has to be recalculated; the pattern's value is
that this recalculation can be done in a single place.

## Summary

- An authentication implementation can leak information even without the correct credential
  ever being known; the leaked information is user existence, and it is measured by point
  count.
- Three channels were counted in an eight-point inventory — a distinct error message, work
  skipped when no record exists, and a comparison that exits on the first mismatch — and five
  points leaked through at least one channel.
- The response-time difference is written not as a raw duration but as a band check: the
  expected ratio is derived from the step count, and the measured value is reported only by
  whether it falls inside the band.
- Constant-time comparison closes only one of the three channels; four of the five leaking
  points stay open. A single error message alone also leaves four open.
- A central validator that closes all three channels at once gives zero leaking points and
  covers a ninth point added with no extra work; in the distributed state, the same point
  raises the uncovered count to 6.
- The coverage count depends on the set of channels being counted: when the lock response is
  added as a fourth channel, the same central validator's uncovered point count rises from 0
  to 2.

## Next Step

This lesson measured the moment identity **is established**. Once authentication succeeds, a
new object appears: the session identifier generated so identity does not have to be asked for
again on subsequent requests. When that identifier is generated, whether it changes when
authentication succeeds, and whether it is actually invalidated server-side when the user logs
out are separate decisions — and each of these decisions is made separately within a flow. The
next lesson derives the flow set: how many flows generate a session identifier, how many of
them fail to renew the old one when they do, and how many sessions continue to live on after
logout when there is no server-side invalidation.
