Search by

yahyaerturan / auth-psr15

yahyaerturan

PSR-15/PSR-7 HTTP integration for yahyaerturan/auth: request-scoped authentication context, session-token extraction and secure session-cookie handling.

Package info

github.com/yahyaerturan/auth-psr15

pkg:composer/yahyaerturan/auth-psr15

Statistics

Installs: 1

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v1.0.0 2026-09-08 22:43 UTC

This package is auto-updated.

Last update: 2026-09-08 23:31:30 UTC


README

PSR-15 and PSR-7 HTTP integration for yahyaerturan/auth.

Middleware that resolves the session credential once per request and attaches an immutable authentication context to it, a fail-closed set of token extractors, and a session-cookie writer that names and enforces every security attribute rather than defaulting it.

composer require yahyaerturan/auth-psr15

Requires PHP 8.5, yahyaerturan/auth, and the PSR interface packages psr/http-message (PSR-7), psr/http-server-middleware and psr/http-server-handler (PSR-15), plus psr/clock (PSR-20). It works with any PSR-7 implementation and any PSR-15 dispatcher.

Why you would install it

yahyaerturan/auth is deliberately framework-agnostic: it knows about sessions and tokens, and nothing about requests, responses, cookies or headers. That is what makes it usable anywhere, and it means somebody has to bridge the two.

This package is that bridge, and it is small on purpose. It does four things:

  1. Extracts a session token from the request — cookie, Authorization: Bearer, or both.
  2. Resolves it to an AuthenticationContext by calling the core's AuthenticateSession.
  3. Attaches that context to the request as an attribute, for handlers downstream.
  4. Writes the session cookie on login, and clears it on logout.

What it does not do is decide anything. There is no RequireAuthentication middleware that issues a 401, and no permission check that issues a 403 — because the right status code depends on whether a resource's existence is itself a secret, and only your application knows that.

Minimal usage

<?php

declare(strict_types=1);

use YahyaErturan\Auth\Psr15\AuthenticationContextRequest;
use YahyaErturan\Auth\Psr15\CookieSessionTokenExtractor;
use YahyaErturan\Auth\Psr15\SessionAuthenticationMiddleware;
use YahyaErturan\Auth\Psr15\SessionCookieConfiguration;

$cookie = SessionCookieConfiguration::hostPrefixed();

// $authenticateSession is the core's AuthenticateSession use case,
// composed from your repositories, clock and token hasher.
$middleware = new SessionAuthenticationMiddleware(
    CookieSessionTokenExtractor::configuredBy($cookie),
    $authenticateSession,
);

Add $middleware to your PSR-15 pipeline. Downstream, any handler reads the context off the request:

$context = AuthenticationContextRequest::require($request);   // throws if absent
$context = AuthenticationContextRequest::find($request);      // null if absent

if ($context?->isAuthenticated() === true) {
    $userId = $context->userId();
}

require() is not an authorization check. It throws when the middleware did not run — a wiring mistake — not when the visitor is anonymous. An anonymous visitor gets a perfectly valid context that reports isAuthenticated() === false.

A complete framework-free composition lives in examples/PlainPsr15Example.php, driven end to end by Psr15ExampleTest so it cannot rot into pseudocode.

Extracting the credential

Extractor Reads
CookieSessionTokenExtractor one named cookie
AuthorizationHeaderSessionTokenExtractor Authorization: Bearer <token>
CompositeSessionTokenExtractor several sources, fail-closed

The composite is the interesting one:

$extractor = new CompositeSessionTokenExtractor(
    CookieSessionTokenExtractor::configuredBy($cookie),
    new AuthorizationHeaderSessionTokenExtractor(),
);

If two sources both present a credential, the request is treated as unauthenticated — even if the two tokens are identical (ADR-055).

That is not fussiness. "Take the first one" is a choice about which credential authenticates, made by whichever extractor happens to be registered first, and an attacker who can set one of the two channels gets to pick. Refusing is the only answer that does not depend on registration order.

Malformed input — a bearer value that is not canonical, a cookie that is not a well-formed token — is refused rather than repaired. Nothing is trimmed and nothing is normalised, because a token that needed repair is not the token that was issued.

Cookies

SessionCookieConfiguration::hostPrefixed();          // recommended
SessionCookieConfiguration::secureCookie($name, $sameSite, $path, $domain);
SessionCookieConfiguration::forPlainHttpDevelopment($name);   // local HTTP only

hostPrefixed() takes no arguments deliberately: every attribute a __Host- cookie may vary is already at its safest value, and the two that could vary — Path and Domain — are the two the prefix fixes.

The __Host- rules are enforced, not documented. A __Host- name with Secure off, a Domain set, or a Path other than / is rejected at construction rather than silently written and quietly ignored by the browser.

forPlainHttpDevelopment() is named at length on purpose. It is the only way this library produces a cookie without Secure, and the call site says so.

Writing and clearing:

$writer = new SessionCookieWriter($cookie, $clock);

$response = $writer->withIssuedSession($response, $createSessionResult);   // login
$response = $writer->withoutSession($response);                            // logout

The writer takes a PSR-20 clock rather than reading the wall clock, so a rendered Max-Age is reproducible in a test. Expiry is truncated toward the past: a cookie never outlives the session it carries.

See docs/COOKIES.md for every attribute, the __Host- constraints in full, and the subdomain trade-off.

Composing the flows

Logout revokes first, then clears the cookie. In that order:

$logout->handle($context->sessionId());          // server-side revocation
$response = $writer->withoutSession($response);  // then the cookie

Clearing the cookie alone leaves a live session that anyone holding the token can still use. The cookie is a convenience for the browser; the session is the truth.

A password change rotates the session — the core issues a new session and invalidates the old, so the response must carry the new cookie or the user is logged out of their own password change.

Full compositions for login, logout, password change and password reset are in docs/HTTP_INTEGRATION.md.

Security-relevant defaults

Two credentials fail closed never "first one wins"
__Host- constraints enforced rejected at construction, not ignored at runtime
Secure and HttpOnly by default opting out requires a method whose name says so
SameSite=Lax by default None is available and requires Secure
Cookie expiry truncated toward the past the cookie never outlives the session
The request attribute is a destination, not a source a forged attribute cannot become an authenticated context (ADR-057)
No raw token is ever attached to the request the context carries identity, never the credential
No superglobals, no setcookie(), no header() every response is a returned object
No ambient clock PSR-20 injected, so expiry is testable

CSRF is your responsibility

This package does not provide CSRF protection, and cannot: the defence depends on your forms, your routing and your templating, none of which a transport adapter can see.

SameSite=Lax — the default here — blocks cross-site POST, which covers the common case. It is not a complete defence: it does not protect same-site subdomain attacks, and SameSite=None (needed for genuine cross-site flows) removes it entirely. If you set None, you need a CSRF token, and you need it from your framework.

Trusted proxies and client identity

This package never reads X-Forwarded-For, X-Real-IP or any other proxy header, and never derives a client IP. Deriving one incorrectly is how a rate-limit key becomes attacker-controlled — anyone who can set a header gets their own bucket.

Client metadata is supplied, never derived (ADR-068). Your application decides which proxies it trusts and passes the result in.

Long-running processes

Safe under Swoole, RoadRunner, FrankenPHP and workers generally, and asserted rather than assumed:

  • no static or global state — a test drives sequential requests through one middleware instance and checks nothing leaks between them;
  • no superglobals — PSR-7 hands the request over already parsed, and a second reader would disagree with the first;
  • no setcookie() or header() — both write to a process-global buffer a PSR-7 response cannot see;
  • no ambient clock — an injected PSR-20 clock, so a worker that has been up for a week has no stale notion of now.

Failure behaviour

Expected denials produce an anonymous context. An expired session, a revoked session, a token that matches nothing: the request continues, unauthenticated. These are ordinary outcomes, not errors.

Infrastructure failures propagate. A database that is unreachable throws, and the exception passes through the middleware untouched.

That distinction is the important one. Swallowing a repository failure into "not authenticated" would make an outage look exactly like a logged-out user — your monitoring sees a spike in anonymous traffic instead of an incident, and every protected page silently becomes a login redirect.

What this package does not own

  • Authorization. No permission check, no 403, no policy. That is yahyaerturan/auth-authorization, and the mapping from a denial to a status code is your application's — see ADR-062 and document 12 §Deny behavior.
  • Authentication logic. Session lookup, expiry, revocation and credential binding are all yahyaerturan/auth. This package calls one use case.
  • Persistence. It touches no storage at all.
  • CSRF, routing, templating, sessions-as-in-$_SESSION. None of these.

Relationship to the rest of the ecosystem

Package Repository Relationship
yahyaerturan/auth https://github.com/yahyaerturan/auth required
yahyaerturan/auth-psr15 https://github.com/yahyaerturan/auth-psr15 this package
yahyaerturan/auth-pdo https://github.com/yahyaerturan/auth-pdo independent — supply any adapter
yahyaerturan/auth-authorization https://github.com/yahyaerturan/auth-authorization independent — this package never calls it
yahyaerturan/auth-testing https://github.com/yahyaerturan/auth-testing dev — in-memory adapters for the suite

This package depends on yahyaerturan/auth and the PSR interfaces, and on no sibling package at runtime.

Development

git clone https://github.com/yahyaerturan/auth-psr15
cd auth-psr15
composer install
composer qa          # platform, coding standard, PHPStan, PHPUnit

No database is needed. The suite composes the whole stack in memory, which is the point: document 19's Phase 7 exit criterion is that a framework-free stack authenticates, and composing it in memory is what makes that a claim about this adapter rather than about a driver.

PSR-7 conformance comes from a real implementation — nyholm/psr7 is a dev dependency — rather than from a hand-written double, because a double would agree with whatever this package assumed (ADR-058).

Working across several packages at once

Clone the repositories you need as siblings, then point Composer at the checkouts without committing anything:

cp composer.json composer.dev.json
composer config --file composer.dev.json repositories.siblings \
    '{"type":"path","url":"../auth*","options":{"symlink":true}}'
composer config --file composer.dev.json minimum-stability dev
COMPOSER=composer.dev.json composer update

composer.dev.json and composer.dev.lock are git-ignored.

Documentation

Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md for the setup, the quality gate, and the conventions this project expects. Participation is governed by the Code of Conduct.

Found a security vulnerability? Do not open an issue or a pull request.

Security

See SECURITY.md.

License

MIT — see LICENSE.