PHPackages                             hydrakit/csrf - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. hydrakit/csrf

ActiveLibrary[Security](/categories/security)

hydrakit/csrf
=============

CSRF library for Hydra PHP framework

v0.2.0(yesterday)018↑50%2MITPHP &gt;=8.2

Since Jul 6Compare

[ Source](https://github.com/hydra-foundation/csrf)[ Packagist](https://packagist.org/packages/hydrakit/csrf)[ RSS](/packages/hydrakit-csrf/feed)WikiDiscussions Synced today

READMEChangelogDependencies (8)Versions (4)Used By (2)

Hydra CSRF
==========

[](#hydra-csrf)

Synchronizer-token CSRF protection: one secret token per session, compared in constant time against whatever an unsafe request submits. State lives entirely in the session, so the guard is stateless and the package ships **no**`ServiceProvider` — the guard autowires from the session and `Signer` bindings.

How it works
------------

[](#how-it-works)

`CsrfGuard::token()` mints a random token lazily on first read (typically when a view renders a form or the layout's meta tag) and returns a stable value for the life of the session. The random token is stored in the session (the synchronizer source of truth); what `token()` **emits** is that value HMAC-signed with `APP_KEY` via `Hydra\Core\Security\Signer`, as `.`. It's all URL/HTML/header-safe characters, so it embeds in HTML attributes, JSON (`hx-headers`), and headers without further encoding. `validate()` is read-only — it verifies the signature first (a forged or tampered value is rejected on a cheap recompute), then compares the recovered token to the stored one with `hash_equals`, and never mints.

Signing is defence-in-depth over the synchronizer store, not a replacement for it, and it makes `APP_KEY` load-bearing on the framework's own security path: the guard takes a `Signer` (bound by the app's `SignerServiceProvider` from `APP_KEY`), so a missing or malformed key fails loud the first time a token is minted. The guard still ships no `ServiceProvider` — it autowires from the session and `Signer` bindings.

`VerifyCsrfTokenMiddleware` lets safe methods (GET/HEAD/OPTIONS) through untouched — a GET is what mints the token in the first place — and requires a matching token on **every other** method (POST/PUT/PATCH/DELETE, but also any WebDAV or custom verb — the safe list is an allowlist, so unknown verbs fail closed), else a 403 before the request reaches a controller. The token is read from the `X-CSRF-Token` header first (htmx and AJAX clients send it automatically once the layout is wired) and falls back to the `_token` form field.

> Place this middleware **inside** the session middleware in the stack — it needs the session started so the guard can read the stored token.

Rotating
--------

[](#rotating)

OWASP recommends rotating the CSRF token on authentication. Note that `SessionInterface::regenerate()` keeps the data, including this token, so the token survives a session-id rotation — call `rotate()` explicitly:

```
public function login(ServerRequestInterface $request): ResponseInterface
{
    // ... verify credentials ...

    $session->regenerate(); // new session id (fixation defence)
    $guard->rotate();       // new CSRF token (regenerate() alone keeps the old one)

    return redirect('/dashboard');
}
```

`rotate()` discards the stored token and mints, stores and returns a fresh one. Tokens embedded in already-rendered pages stop validating immediately, so call it where you're navigating anyway (the login POST handler).

Expired sessions: redirect instead of a bare 403
------------------------------------------------

[](#expired-sessions-redirect-instead-of-a-bare-403)

When a session expires (or its cookie is cleared) and the user then submits a form they loaded earlier, the fresh session knows no token, so the middleware rejects the POST — and without help the user sees a bare 403 where they expected the login page.

The package's part is the **mechanism**: the middleware throws a distinct typed exception, `Hydra\Csrf\Exceptions\TokenMismatchException` (a 403 `HttpException`), and never builds a response itself. What to DO about a mismatch is **policy**, and policy belongs to the app — the package owns no routes and cannot know where "log in" lives.

The recipe: an app middleware placed **outside** `VerifyCsrfTokenMiddleware`(it can only catch what it wraps) but inside the session middleware catches the exception and asks `CsrfGuard::issued()` — read-only, never mints — whether the session has a token at all:

- **no token issued** → the session cannot possibly validate anything: the submit came from a form rendered by a session that no longer exists — the expired-session symptom. Redirect to the login page.
- **token issued but mismatched** → the session is alive and knows its token, so the bad submission is a real CSRF failure (or a page rendered before an explicit `rotate()`). Rethrow — the 403 is correct, and swallowing it would blunt the protection.

```
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    try {
        return $handler->handle($request);
    } catch (TokenMismatchException $e) {
        if ($this->csrf->issued()) {
            throw $e; // a live session with a bad token IS a CSRF failure: 403
        }

        return $this->respond->redirect('/login'); // expired session: log in again
    }
}
```

Deciding by `issued()` (not by asking the auth guard whether a user is logged in) keeps the policy middleware session-only and cheap: resolving the auth guard would drag the app's user provider — and its database connection — into every request just to serve a catch block that almost never fires. The reference app's `RedirectUnauthenticatedMiddleware` implements exactly this recipe (folded into the same middleware that maps auth's 401 to a redirect).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community5

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

3

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/a947272da0bf1d7d6326b4fbf6ddb8049977cbdbe333d0d1282dbb6f5927dfdf?d=identicon)[whleucka](/maintainers/whleucka)

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/hydrakit-csrf/health.svg)

```
[![Health](https://phpackages.com/badges/hydrakit-csrf/health.svg)](https://phpackages.com/packages/hydrakit-csrf)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.9k19.5M1.8k](/packages/cakephp-cakephp)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[cakephp/authentication

Authentication plugin for CakePHP

1214.1M106](/packages/cakephp-authentication)[typo3/cms-core

TYPO3 CMS Core

3713.2M5.2k](/packages/typo3-cms-core)[sunrise/http-router

A powerful solution as the foundation of your project.

17451.8k10](/packages/sunrise-http-router)[typo3/cms-adminpanel

TYPO3 CMS Admin Panel - The Admin Panel displays information about your site in the frontend and contains a range of metrics including debug and caching information.

115.7M68](/packages/typo3-cms-adminpanel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
