Lesson 15 / 15
Authentication Flows
Measuring the identity of a request in the session-based and the bearer-token flow, seeing what a signature protects and what it does not, and gathering the fifteen lessons of the course into one table.
Published Updated
In this lesson
- Builds the session-based flow and the signed bearer-token flow side by side and classifies the identity of the request with the party measurement
- Produces and verifies a token of the form user.signature with hash_hmac() and hash_equals()
- Measures that a session can be revoked on the server while a signed token stays valid as long as the key does not change
- Reads the fifteen lessons of the course in one table by party class
Contents
The Password Storage lesson measured the stored value and the setting that produces it. Once the password is verified, one question remains: on behalf of which user are the following requests processed, and who determines that? Authentication establishes who the user is; authorization, which establishes what the user may do, is a separate matter and was covered in the Authentication and Authorization lesson in the context of the framework. There the measurement was whether the gate runs the request. Here every part runs; the question is who can change the identity of the request.
SE6. In this lesson the password is only verified. Guessing passwords, stealing sessions, forging tokens and exceeding permissions are not modeled. The client’s deviation is always a neutral value: a session ID or a token that another user obtained from that user’s own login.
Two Flows
HTTP is stateless; the server sees every request separately. The identity is attached to the request in two ways. In the session-based flow, after login the server generates a random session ID, keeps the record from the ID to the user in its own memory and hands the ID to the client; the client sends it back with every request. In the bearer-token flow, the server keeps no record: it signs the user name with hash_hmac() and hands out a bearer token of the form user.signature. The client carries the token with every request, for example in the Authorization header; the server recomputes the signature and compares it with hash_equals().
The measurement core is the same as in the previous lessons: probe() computes the result once without deviation, then with each external party’s deviation; party() looks at the client, the third party and the environment in order, and names the server if none could change it. The file is given unchanged, as in every lesson.
<?php // party.php — who can change this result? declare(strict_types=1); // The parties outside the code, in decision order: every request, every external call, every deployment. const EXTERNAL_PARTIES = ['client', 'third party', 'environment']; /** Computes the result once without deviation, then once with each external party's deviation. */ function probe(callable $result, array $deviations): array { $baseline = $result([]); $changed = []; foreach (EXTERNAL_PARTIES as $party) { if (array_key_exists($party, $deviations)) { $changed[$party] = $result([$party => $deviations[$party]]) !== $baseline; } } return $changed; } /** The first external party that can change the result; the server if none can. */ function party(array $changed): string { foreach (EXTERNAL_PARTIES as $p) { if ($changed[$p] ?? false) return $p; } return 'server'; } function partyReport(string $name, callable $result, array $deviations): string { $changed = probe($result, $deviations); $lines = []; foreach ($changed as $p => $c) $lines[] = $p . ($c ? ' changed' : ' could not change'); $owner = party($changed); echo "$name → $owner\n ", implode(' · ', $lines), "\n"; return $owner; }
The file below builds the two flows. Passwords are tested only with password_verify(), the session ID is generated with random_bytes(), and the token signature is made with hash_hmac('sha256', ...). The signing key here is a teaching string; in a real application the key does not sit in the code but is read from an environment variable.
<?php // identity.php — session and token flows declare(strict_types=1); const SIGNING_KEY = 'lesson-key'; function hashes(): array { $hashes = []; foreach (['alice' => 'password-one', 'bob' => 'password-two'] as $user => $password) { $hashes[$user] = password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]); } return $hashes; } function verifyLogin(array $hashes, string $user, string $password): bool { return isset($hashes[$user]) && password_verify($password, $hashes[$user]); } final class SessionStore { private array $records = []; public function open(string $user): string { $id = bin2hex(random_bytes(16)); $this->records[$id] = $user; return $id; } public function user(string $id): string { return $this->records[$id] ?? 'anonymous'; } public function close(string $id): void { unset($this->records[$id]); } } function issueToken(string $user, string $key = SIGNING_KEY): string { return $user . '.' . hash_hmac('sha256', $user, $key); } function tokenUser(string $token, string $key = SIGNING_KEY): string { $parts = explode('.', $token, 2); if (count($parts) !== 2) return 'anonymous'; $expected = hash_hmac('sha256', $parts[0], $key); return hash_equals($expected, $parts[1]) ? $parts[0] : 'anonymous'; }
The Request Identity
The result is the user name the request is processed for. The client’s deviation is presenting a credential that another user obtained from that user’s own login: bob’s session ID in the session flow, bob’s token in the token flow. Random values are not printed; only their lengths and the results are.
<?php // flow.php — request identity, two flows declare(strict_types=1); require 'party.php'; require 'identity.php'; $hashes = hashes(); foreach (['alice' => 'password-one', 'bob' => 'password-two'] as $user => $password) { echo "login $user ", var_export(verifyLogin($hashes, $user, $password), true), "\n"; } echo 'wrong password ', var_export(verifyLogin($hashes, 'alice', 'password-two'), true), "\n"; $store = new SessionStore(); $aliceSession = $store->open('alice'); $bobSession = $store->open('bob'); $aliceToken = issueToken('alice'); $bobToken = issueToken('bob'); echo 'session ID ', strlen($aliceSession), ' characters · signature ', strlen(explode('.', $aliceToken)[1]), " characters\n"; partyReport('identity (session)', fn(array $d) => $store->user($d['client'] ?? $aliceSession), ['client' => $bobSession]); partyReport('identity (token)', fn(array $d) => tokenUser($d['client'] ?? $aliceToken), ['client' => $bobToken]); echo 'mismatched signature ', tokenUser('alice.' . str_repeat('0', 64)), "\n"; echo 'unrecognized session ID ', $store->user(str_repeat('0', 32)), "\n";
login alice true
login bob true
wrong password false
session ID 32 characters · signature 64 characters
identity (session) → client
client changed
identity (token) → client
client changed
mismatched signature anonymous
unrecognized session ID anonymous
In both flows the result belongs to the client. When the client changed the session ID or the token it presented, the identity of the request moved from alice to bob. The one who carries the identity to the request is the client, who sends it with every request; the server only verifies the presented information. Verification works differently at two separate points of the same flow: in the session flow the server looks the ID up in its own record, in the token flow it computes the signature and compares.
SE7. The last two lines show the limit of the defense. A token with a mismatched signature did not produce a user name, and neither did an unrecognized session ID: both gave anonymous. The client cannot choose a user name; it can present one of the credentials the server gave it, or none. So the defense does not take the result from the client, it narrows the set the client can choose from to the values the server issued. This is the sentence repeated since the start of the course: a defense does not always change the owner, sometimes it only narrows what can be changed.
The Cookies lesson showed the same thing with a signed cookie. The mechanism that carried the cart to the server in the Sessions lesson does not carry the identity here, because the identity itself is a value in the client’s hands; the limit of the Sessions lesson was the same: which session is opened stays with the client.
The Bounding Measurement
The two flows fell on the same party, but the remedy in the server’s hands is not the same. In the session flow, when the server deletes the record, the ID no longer maps to any user. In the token flow the server keeps no record, so there is nothing to delete; the token stays valid as long as the signing key does not change. If the key comes from the environment, the one who changes it is the deployment, and the change drops all the tokens at once.
<?php // logout.php — logout and key change declare(strict_types=1); require 'party.php'; require 'identity.php'; $store = new SessionStore(); $session = $store->open('alice'); $token = issueToken('alice'); echo 'before logout session ', $store->user($session), ' · token ', tokenUser($token), "\n"; $store->close($session); echo 'after logout session ', $store->user($session), ' · token ', tokenUser($token), "\n"; partyReport( 'identity (key from environment)', fn(array $d) => tokenUser($token, $d['environment'] ?? SIGNING_KEY), ['environment' => 'new-key'] );
before logout session alice · token alice
after logout session anonymous · token alice
identity (key from environment) → environment
environment changed
After logout the session gave anonymous, and the token kept giving alice. An application that wants the token to disappear at logout has to add a revocation record on the server, and that gives back the advantage of the token being recordless. When the key changed, the token dropped, and the one who changed the result is the environment: the deployment determines the key. SE8. Time is not measured in this lesson; a token carrying an expiry or a session closing when idle requires a clock and is outside the scope of the measurement. Only which remedy the server has was examined here.
Summary
- The measured class of this lesson is the client: the client that changed the session ID or the token changed the identity of the request, in both flows.
- The session flow resolves the identity from a record in server memory, and the token flow verifies the
hash_hmac()signature withhash_equals(); both reduce an unrecognized value toanonymous. - The signature limits the set the client can choose from to the tokens the server issued; it does not change the owner.
- A session is revoked on the server; a signed token stays valid as long as the key does not change, and if the key comes from the environment, the one who changes it is the environment.
Course Wrap-Up
In fifteen lessons, this course measured who can change a result: sessions and state, data access, security. Each lesson carried its own result and a contrasting or bounding measurement. The table below gathers all of them in one place; the party cells are taken from the summary of the source lesson.
| Lesson | Result measured | Party | Deviation or contrasting measurement |
|---|---|---|---|
| Cookies | The cart in the cookie, signed cookie | client | The signature noticed the changed value, did not change the owner |
| Sessions | The cart in $_SESSION |
server | The same cart=5 cookie changed the result |
| Configuration and Environment Variables | The connection name with APP_DATABASE |
environment | A value written in the code stays on the server, not carried |
| The Database Access Layer | The type of the returned value | environment | With an explicit int cast, the server |
| Transactions | The sum of the balances of two accounts | server | In a non-transactional transfer, the environment changed the sum |
| Migration Management | The number of applied migrations | environment | The schema version is the same in every environment: server |
| Queries Behind Object–Relational Mapping | The number of queries sent | server | With lazy loading it grew with the rows: client |
| Data Formats | The parsed price | third party | The number representation in our own JSON: server |
| The HTTP Client | The price reported by the service | third party | The fallback value bounds the result, does not change the owner |
| Input Validation and Sanitization | The validated quantity | client | Sanitization turns malformed input into another value |
| Preventing SQL Injection | The text sent to prepare() |
server | Under concatenation, the client |
| Preventing Cross-Site Scripting | The printed HTML | client | Double escaping breaks the text |
| Request Forgery Protection | The email update | server | In a tokenless update, the third party |
| Password Storage | The hash prefix | environment | At an explicit cost, the server |
| Authentication Flows | The request identity | client | When the key comes from the environment, the environment |
The table holds two separate distributions, and they count different things. The distribution of the lessons counts the measured class of each lesson: 5 server · 4 environment · 4 client · 2 third party. The distribution of the core counts the ten results run in the Cookies lesson: 4 server · 2 environment · 2 client · 2 third party. One is the distribution of fifteen lessons, the other of ten measurements; they are not the same numbers, and one does not verify the other.
The course makes a single claim: in a web application, the owner of a result is not the one who wrote the code. The client brings the request, the environment brings the setting, the third party brings the data; defense mechanisms sometimes move the owner to the server and sometimes only narrow it. The next course takes this application to the production environment: deployment, running and performance.
To keep your progress and take notes, Log in
My notes
Log in to take notes.