Cookies & Encryption#

Read and set cookies safely. Optionally enable transparent encryption so values at rest (client-side) are unreadable without your key.


Reading cookies (request)#

use Infocyph\Webrick\Request\Request;
use Infocyph\Webrick\Response\Response;

Route::get('/cookie/read', function (Request $r) {
    return Response::json([
        'all'    => $r->getCookieParams(),
        'demo'   => $r->cookie('demo'),   // null if missing
    ]);
});

If CookieEncryptionMiddleware is enabled, cookie('name') returns the decrypted value.


Setting cookies (response)#

Set cookies explicitly via Set-Cookie header (clear & PSR friendly):

use Infocyph\Webrick\Response\Response;

Route::get('/cookie/set', function () {
    $cookie = rawurlencode('demo') . '=' . rawurlencode('secret-value')
            . '; Path=/; HttpOnly; SameSite=Lax; Secure';
    return Response::json(['ok'=>true])->withAddedHeader('Set-Cookie', $cookie);
});

Attributes you’ll commonly use

  • Path=/ – cookie visible to entire site

  • Domain=example.com – cross-subdomain sharing if needed

  • Expires=Wed, 31 Dec 2030 23:59:59 GMT / Max-Age=3600 – persistence

  • HttpOnly – hide from JS (mitigates XSS cookie theft)

  • Secure – HTTPS only

  • SameSite=Lax|Strict|None – CSRF boundary (set None only with Secure)


Encrypted cookies (optional)#

Enable the middleware once (pre-global) and pass a secret key:

$preGlobal[] = new \Infocyph\Webrick\Middleware\CookieEncryptionMiddleware(
    $_ENV['WEBRICK_COOKIE_KEY'] ?? 'change-me'
);
  • Write cookies as usual (plain).

  • Read via $r->cookie('name') → decrypted value if that cookie was written encrypted.

  • Rotation: deploy a new key and keep the previous key for a grace period if the middleware supports multi-key decryption; otherwise rotate during a maintenance window.

Use encrypted cookies for any value you don’t want end users to read or tamper with (session info, feature flags with risks). Pair with server-side verification when integrity matters.


Deleting cookies#

Overwrite with an expired cookie (and same scope):

Route::get('/cookie/clear', function () {
    $expired = 'demo=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Lax';
    return Response::json(['ok'=>true])->withAddedHeader('Set-Cookie', $expired);
});

Scoped cookies (subdomains & paths)#

  • Path scoping: Path=/admin limits visibility to /admin/*.

  • Domain scoping: Domain=.example.com shares across subdomains (use carefully).

  • For domain-scoped routes (e.g., api.example.com), set the cookie domain explicitly to match where the browser should send it.


Security notes#

  • Treat cookies as untrusted input on the server; validate and sanitize even when encrypted.

  • Don’t store PII or secrets client-side unless absolutely necessary and encrypted.

  • Use HttpOnly + Secure + SameSite appropriately (often Lax for app cookies; Strict for sensitive flows).

  • For login/CSRF-sensitive POST routes, combine SameSite with CSRF tokens.


Examples#

Remember-me (hint)#

Use a signed, encrypted token (not just a user ID). Validate server-side and rotate on use.




Testing Cookies#

Unit Test#

use PHPUnit\Framework\TestCase;

class CookieTest extends TestCase
{
    public function testSetsCookieWithCorrectAttributes(): void
    {
        $response = Response::json(['ok' => true])
            ->withAddedHeader('Set-Cookie',
                'test=value; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600'
            );

        $setCookie = $response->getHeaderLine('Set-Cookie');

        $this->assertStringContainsString('test=value', $setCookie);
        $this->assertStringContainsString('HttpOnly', $setCookie);
        $this->assertStringContainsString('Secure', $setCookie);
        $this->assertStringContainsString('SameSite=Lax', $setCookie);
    }

    public function testReadsCookieFromRequest(): void
    {
        $request = Request::fake(
            headers: ['Cookie' => 'sess=abc123; theme=dark'],
            method: 'GET',
            uri: '/',
        );

        $this->assertEquals('abc123', $request->cookie('sess'));
        $this->assertEquals('dark', $request->cookie('theme'));
        $this->assertNull($request->cookie('nonexistent'));
    }
}

Integration Test#

#!/bin/bash
# test-cookies.sh

set -e

BASE_URL="http://localhost:8000"

echo "Testing cookie functionality..."

# Test 1: Set cookie
RESPONSE=$(curl -s -c cookies.txt -b cookies.txt "$BASE_URL/login" \
  -d "[email protected]&password=secret" \
  -w "\n%{http_code}")

HTTP_CODE=$(echo "$RESPONSE" | tail -1)
if [ "$HTTP_CODE" = "200" ]; then
    echo "✅ Cookie set on login"
else
    echo "❌ Login failed (HTTP $HTTP_CODE)"
    exit 1
fi

# Test 2: Cookie persists
RESPONSE=$(curl -s -b cookies.txt "$BASE_URL/profile" -w "\n%{http_code}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)

if [ "$HTTP_CODE" = "200" ]; then
    echo "✅ Cookie persists across requests"
else
    echo "❌ Cookie not sent or invalid (HTTP $HTTP_CODE)"
    exit 1
fi

# Test 3: Cookie cleared on logout
curl -s -c cookies.txt -b cookies.txt "$BASE_URL/logout" > /dev/null

RESPONSE=$(curl -s -b cookies.txt "$BASE_URL/profile" -w "\n%{http_code}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)

if [ "$HTTP_CODE" = "401" ]; then
    echo "✅ Cookie cleared on logout"
else
    echo "❌ Cookie still valid after logout (HTTP $HTTP_CODE)"
    exit 1
fi

rm -f cookies.txt
echo "✅ All cookie tests passed"

Browser DevTools Inspection#

Chrome/Edge DevTools#

  1. Open DevTools (F12)

  2. Go to Application tab

  3. Expand Cookies in left sidebar

  4. Select your domain

What to Check:

  • HttpOnly column is checked for session cookies

  • Secure column is checked (in production)

  • SameSite is set appropriately

  • Expires / Max-Age matches expectation

  • Path is scoped correctly

  • Domain is minimal (or blank for same-origin)

Firefox DevTools#

  1. Open DevTools (F12)

  2. Go to Storage tab

  3. Expand Cookies

  4. Select your domain


Checklist#

  • Prefer HttpOnly; Secure; SameSite on all cookies

  • Encrypt sensitive values with CookieEncryptionMiddleware

  • Keep cookie scope minimal (Path/Domain)

  • Clear cookies by setting Max-Age=0 / past Expires

  • Never trust cookie contents without validation