---
title: 'Authentication Flows'
source: 'https://academia.sh/en/courses/php-web/authentication-flows'
course: 'PHP on the Web: State, Data and Security'
language: en
updated: '2026-10-21T05:03:37+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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
<?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
<?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
<?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
<?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 with `hash_equals()`; both reduce an unrecognized value to `anonymous`.
- 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.
