Skip to content
academia.sh

Lesson 10 / 15

Authentication and Authorization Distinction

Two controls are placed in separate scopes over the same step set and measured against two separate requests: a forged-identity request and a verified-but-unauthorized request. How many paths putting authentication at every step closes against a verified-but-unauthorized request, and how many paths asking authorization only at the data layer opens, are counted.

Published Updated

In this lesson

  • Ties crossing a step to two separate controls and runs each control with its own scope
  • Measures two different requests in the same scheme: a forged-identity request and a verified-but-unauthorized request
  • Computes how many paths widening authentication closes against a verified-but-unauthorized request
  • Counts which assets asking authorization only at the data layer leaves open
Contents

The previous lesson checked identity at every step and brought the path count down from fifty-five to twenty. The question asked was always the same: who sent this request? But crossing a step requires answering two separate questions. Authentication determines who sent the request; authorization determines whether that identity can perform this action on this item. The two questions are different, look at different data, and can be asked in different places.

The Authentication and Authorization course covered this distinction at the flow and token level. What is measured here is not the mechanism but the path a control opens by sitting in the wrong layer. The measurement runs against two separate requests: one with a forged identity, and one with a real identity but unauthorized for that step. The second is this lesson’s real subject, because most controls only see the first. A forged-identity request represents an outside attacker; a verified-but-unauthorized request represents a compromised legitimate identity or a misconfigured service — and the second case is both more common and harder to see than the first.

  • DP22. A step passes through two controls. If it is in the authentication scope, it requires the request’s identity to be valid; if it is in the authorization scope, it requires that identity to be authorized for the step.
  • DP23. The authorization set is derived from the legitimate flow list; an identity is authorized on the steps of its own flows and on the shared internal flow steps.
  • DP24. The two request profiles are run separately. A forged-identity request cannot pass authentication; a verified-but-unauthorized request passes all of authentication and can only be stopped by authorization.
  • DP25. The daily control count is the sum of both controls: if both run on one step, that counts as two operations. The controls’ duration and latency are not modeled.
  • DP26. Authorization always makes the correct decision. A wrongly written authorization set, a stale record, and a delegated session are not modeled; these would grow the count of opened paths.

Two Questions, Two Scopes

The graph and the legitimate flow list have not changed. The only thing that changes is which question is asked at which step.

// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
// control.mjs — two separate questions: authentication and authorization, with separate scopes
import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs';
export const EDGE_NAME = EDGE.map(([a, b]) => edgeName(a, b));
const SHARED = new Set(FLOW.filter(([, d]) => !ENTRY.includes(d[0])).flatMap(([, d]) => steps(d)));
const own = (g) => FLOW.filter(([, d]) => d[0] === g).flatMap(([, d]) => steps(d));
// AUTHORIZATION[g]: the steps identity g is authorized for; derived from the legitimate flow list.
export const AUTHORIZATION = Object.fromEntries(ENTRY.map((g) => [g, new Set([...SHARED, ...own(g)])]));
export const PERIMETER = EDGE_NAME.filter((k) => ENTRY.includes(k.split('>')[0]));
export const TO_ASSET = EDGE_NAME.filter((k) => ASSET.includes(k.split('>')[1]));
const neighbor = new Map();
for (const [a, b] of EDGE) neighbor.set(a, [...(neighbor.get(a) || []), b]);
// A step passes through two separate controls; each control has its own scope.
export function path(g, idScope, authScope, identityValid) {
  const ID = new Set(idScope), AUTH = new Set(authScope), out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) {
      const e = edgeName(d, k);
      if (ID.has(e) && !identityValid) continue;         // authentication: who are you
      if (AUTH.has(e) && !AUTHORIZATION[g].has(e)) continue; // authorization: can you do this
      if (!y.includes(k)) walk(k, [...y, k]);
    }
  };
  walk(g, [g]);
  return out;
}
// run1.mjs — with two controls in different layers, which request stops, which passes
import { FLOW, ENTRY, steps } from './network.mjs';
import { EDGE_NAME, PERIMETER, TO_ASSET, path } from './control.mjs';

const SCHEME = [['no-control', [], []], ['identity-perimeter', PERIMETER, []],
  ['identity-every-step', EDGE_NAME, []], ['identity-perimeter-auth-data', PERIMETER, TO_ASSET],
  ['identity-perimeter-auth-everywhere', PERIMETER, EDGE_NAME], ['auth-only-everywhere', [], EDGE_NAME],
  ['both-everywhere', EDGE_NAME, EDGE_NAME]];
const daily = FLOW.reduce((s, [, , n]) => s + n, 0);
console.log('scheme                              forged  verified  asset  daily control  per request');
for (const [nm, idScope, authScope] of SCHEME) {
  const forged = ENTRY.reduce((s, g) => s + path(g, idScope, authScope, false).length, 0);
  const verified = ENTRY.flatMap((g) => path(g, idScope, authScope, true));
  const ID = new Set(idScope), AUTH = new Set(authScope);
  const ctrl = FLOW.reduce((s, [, d, c]) => s + c * steps(d)
    .reduce((t, k) => t + (ID.has(k) ? 1 : 0) + (AUTH.has(k) ? 1 : 0), 0), 0);
  console.log(`${nm.padEnd(36)}${String(forged).padEnd(8)}${String(verified.length).padEnd(13)}` +
    `${String(new Set(verified.map((p) => p.at(-1))).size).padEnd(8)}${String(ctrl).padEnd(16)}${(ctrl / daily).toFixed(2)}`);
}
scheme                              forged  verified  asset  daily control  per request
no-control                          55      55           4       0               0.00
identity-perimeter                  0       55           4       2018            0.57
identity-every-step                 0       55           4       7019            2.00
identity-perimeter-auth-data        0       50           4       4057            1.15
identity-perimeter-auth-everywhere  0       20           4       9037            2.57
auth-only-everywhere                20      20           4       7019            2.00
both-everywhere                     0       20           4       14038           4.00

The second and third rows are this lesson’s main finding. When authentication is widened from the perimeter to every step, daily control rises from two thousand eighteen to seven thousand nineteen — from zero point five seven to exactly two extra steps per request. A forged-identity request finds zero paths in both rows; it was already stopped at the perimeter. A verified-but-unauthorized request, though, uses all fifty-five of the paths in both rows. Tripling and a half the authentication control did not close a single path against a verified request. This is a widening made in the layer of the wrong question, not the wrong layer.

The fourth row is the placement most commonly seen in practice: identity at the gate, authorization at the data layer. Five of the fifty-five paths close, fifty stay open, and all four of the four assets are reached. The reason for asking authorization only at the last step is understandable — the information the authorization decision needs sits there — but the result is that the intermediate steps are never questioned at all. A verified identity moves freely among the intermediate nodes and meets a question only at the asset’s door.

The fifth and seventh rows have to be read together. Putting authorization at every step brings the path down to twenty; putting authentication at every step too gives the same twenty. The difference is only in cost: two point five seven versus four extra steps per request. Zero paths close in exchange for one point four three extra steps. Leaving authentication at the perimeter and spreading authorization gives the same result as spreading both, and costs roughly a third less.

The sixth row shows the other end of the distinction. When authorization is asked at every step and identity is never asked, a forged-identity request finds twenty paths. Authorization answers “can this identity do this”; it does not ask whether the identity really is who it claims. The moment a claimed identity is accepted as true, authorization grants that claim’s every privilege. Neither control substitutes for the other: without one, the other just hands the open path over to a different kind of request.

When Authorization Is Asked at the Last Step

Whose hands hold the fourth row’s fifty? When the same measurement is done per identity, what authorization sees and does not see comes apart.

// run2.mjs — with authorization only in the data layer, where does a verified-unauthorized request reach
import { ENTRY, ASSET } from './network.mjs';
import { PERIMETER, TO_ASSET, AUTHORIZATION, path } from './control.mjs';

console.log(`identity               authorized step  path  ${ASSET.map((v) => v.slice(0, 8).padEnd(10)).join('')}`);
const closed = [];
for (const g of ENTRY) {
  const y = path(g, PERIMETER, TO_ASSET, true), reached = new Set(y.map((p) => p.at(-1)));
  const open = new Set(y.map((p) => p.join('>')));
  closed.push(...path(g, [], [], true).map((p) => p.join(' > ')).filter((p) => !open.has(p.replaceAll(' > ', '>'))));
  console.log(`${g.padEnd(23)}${String(AUTHORIZATION[g].size).padEnd(17)}${String(y.length).padEnd(6)}` +
    ASSET.map((v) => (reached.has(v) ? 'yes' : 'no').padEnd(10)).join(''));
}
console.log(`\n${closed.length} path the data-layer control closes:`);
for (const p of closed) console.log(`  ${p}`);
const narrow = ENTRY.flatMap((g) => path(g, PERIMETER, [...TO_ASSET, ...PERIMETER], true));
console.log(`\nopen path when authorization is data layer + perimeter: ${narrow.length}`);
identity               authorized step  path  measurem  invoice-  subscrib  configur  
field-endpoint         9                8     yes       yes       yes       no        
branch-network         14               23    yes       yes       yes       yes       
public-endpoint        9                5     yes       yes       yes       no        
maintenance-endpoint   10               14    yes       yes       yes       no        

5 path the data-layer control closes:
  field-endpoint > collector > measurement-store
  field-endpoint > work-order > subscriber-registry
  maintenance-endpoint > management > collector > measurement-store
  maintenance-endpoint > management > work-order > subscriber-registry
  maintenance-endpoint > management > configuration

open path when authorization is data layer + perimeter: 46

The branch network row shows how blind authorization is at the data layer. Twenty-three of the twenty-three paths are open; not a single one closed. The reason is the shape of the authorization set: for its own legitimate work, the branch network identity carries one-step authority on all four of the four assets — it reads the measurement store, writes to the subscriber registry while opening a work order, makes configuration changes, and writes to the invoice database through internal flows. Because the data-layer control only looks at the last step, once that last step is authorized, how the request got there is never asked at all. The authorized-step column is the sign of this result: at the widest authorization set — fourteen steps — the data-layer control stops zero paths. The identity with the widest authorization is, in a system checked only at the last step, effectively never checked.

The public endpoint is one of the two narrowest identities and still advances on all five of five paths; the field endpoint drops from ten to eight, the maintenance endpoint from seventeen to fourteen. The five closed paths are listed, and all of them share one trait: every one is a short path, two or three steps long. None of the long paths close.

The reason is who owns the last step. The long paths enter the asset through the validator or billing; these two services’ asset-writing steps belong to internal flows, and internal flow steps are shared by every identity. Whenever the data-layer control looks at the last step, it always finds a step that is authorized. The short paths, by contrast, enter the asset directly, and their last steps belong to a specific identity; that is where a mismatch becomes visible. Lengthening the path is how you escape the data-layer control — and this is not a gap in the control, it is the consequence of where it is placed.

The last row measures a middle option. When authorization is placed at the perimeter as well as the data layer, the open path drops from fifty to forty-six. Four more paths closed — that is the return on adding an eight-step control. Widening authorization does not close paths linearly; how much closes depends on which step the control sits at, and the middle steps carry the most paths. Placing authorization at the ends cuts the path’s ends and leaves the middle open; the twenty in the first table is only reached once the middle steps are brought into the check too.

Together the two tables give a rule of placement. Authentication can be done in one place, because its answer does not change along the path: whoever a request is, it stays that all the way through. Authorization has to be re-asked at every step, because its answer changes from step to step: the same identity is authorized at one step and not at the next. This asymmetry explains why the two controls belong to different layers, and it also justifies the table’s cost difference — leaving identity at the perimeter and spreading authorization gave the same twenty paths for two point five seven extra steps per request instead of four.

A limit of the measurement also has to be written down here. The model assumed the authorization set is correct and is evaluated correctly at every step. In reality, making an authorization decision at an intermediate step requires that step to have the information the decision needs: on whose behalf the work is being done, which item is requested, and which action is being taken. If this information is not carried along the path, authorization cannot be asked at an intermediate step — and if it is asked anyway, the answer is “I do not know,” and that answer turns into letting the request through. The shared-step problem from the previous lesson and the intermediate-step problem here are two faces of the same gap: if who a request is acting for and what it is doing are not carried between steps, no intermediate control can make the right decision. The twenty in the table is therefore not a target, it is a lower bound reachable only once the flows carry this information.

Summary

  • Crossing a step depends on two separate questions, and the two questions stop different kinds of requests: authentication stops a forged identity, authorization stops a verified-but-unauthorized request.
  • Widening authentication from the perimeter to every step raises daily control from 2,018 to 7,019 (0.57 to 2.00 per request) and closes 0 paths against a verified-but-unauthorized request.
  • With identity at the gate and authorization at the data layer, 50 of the 55 paths stay open, and all 4 of the 4 assets are reached. If the last step is authorized, how the request got there is never asked.
  • For the branch network identity, the data-layer control closes none of the 23 paths; by its own legitimate work it carries one-step authority on all 4 of the 4 assets.
  • Putting authorization at every step brings the path to 20 (2.57 per request). Putting identity at every step too gives the same 20 but rises to 4.00 per request: 0 paths for 1.43 extra steps.
  • When authorization is asked at every step and identity is never asked, a forged-identity request finds 20 paths. Neither control substitutes for the other; if one is missing, the open path is handed to a different kind of request.

Next Step

This lesson separated the two controls and counted which request each one stops. Every scheme shared one assumption: the scope list is written correctly, and every step that should be in scope is in scope. In reality this list is hand-written, gets updated as the network changes, and one item gets forgotten. What happens to a forgotten item depends on the control’s default: if a step outside scope lets requests through, forgetting it opens a path; if it stops them, forgetting it stops a job. That is what the next lesson measures: what the path count is under an open default and a closed default, what forgetting a single item costs in paths and stopped jobs, and which of the two errors shows itself.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close