Skip to content
prod e051e98
Browse

4 · Accessibility & cookie compliance

Objective — run the WCAG 2.1 AA checks (automated scanners + manual verification) and build the GDPR/CCPA cookie inventory with consent gating that actually blocks non-essential cookies — the audits that keep the app usable for everyone and keep it legal.

The audits that keep the app usable for everyone — and keep it legal. Automated tools catch the deterministic failures; the judgment-heavy checks (focus order, banner behavior) still need human eyes.

Run automated scanners (WAVE, axe DevTools) on key pages, then verify by hand what tools can’t.

CheckRequirement
Keyboard navLogical tab order, visible focus, no traps
Focus indicatorNever removed globally; :focus { outline: 2px solid … }
ImagesDescriptive alt; alt="" for decorative
Form inputsEvery visible input has a <label for> or aria-label
Color contrast4.5:1 normal text · 3:1 large text & UI
Icon buttonsaria-label or .sr-only text
StructureSemantic landmarks, skip link, sequential headings

Agent-runnable greps for the deterministic failures (the scanners catch the rest):

Terminal window
# Images missing alt text
grep -rn "<img" resources/views/ --include="*.blade.php" | grep -v "alt=" || echo "OK all <img> have alt"
# Inputs that may lack a label (excluding hidden)
grep -rn "<input" resources/views/ --include="*.blade.php" | grep -v 'type="hidden"' | head -20
# Icon-only elements missing an accessible name
grep -rn 'class=".*fa-' resources/views/ --include="*.blade.php" | grep -v "aria-" || echo "OK icons labelled"
# Landmark / semantic elements present in layouts
grep -rn "<main\|<nav\|<header\|<footer" resources/views/layouts/ --include="*.blade.php"

Then add visible focus styles (never remove the outline globally) and a skip link to #main-content.

  1. Scan key pages, then verify the manual checks by hand against every row of the table above.

    • ✅ Automated scanners are clean and the manual checks (tab order, focus, contrast) all pass.

Inventory every cookie, then verify consent actually gates the non-essential ones.

  1. Inventory all cookies (name, expiry, HttpOnly, Secure, SameSite) and classify them Essential / Functional / Analytics / Marketing.

    • ✅ Every cookie is listed with its attributes and a category.
  2. Verify session security — session cookies need HttpOnly=true, Secure=true, SameSite=lax.

    Terminal window
    php artisan tinker --execute="echo json_encode(\Illuminate\Support\Arr::only(config('session'), ['secure','http_only','same_site']));"
    # Expected: {"secure":true,"http_only":true,"same_site":"lax"} (production/staging APP_URL=https://…)
    • ✅ Runtime session config reports secure, http_only, and same_site=lax (not just env() lines in config/session.php).
  3. Scan for custom cookies in app code and enforce the lifetime ceilings. Every custom cookie must carry the same security attributes as the session cookie.

    Terminal window
    grep -rnE "Cookie::make|cookie\(|setcookie|->cookie\(" app/ routes/ --include="*.php"
    # Expected: each hit uses HttpOnly + Secure + SameSite; none sets a raw long-lived plaintext cookie

    Lifetime ceilings:

    CookieMax lifetime
    Session120 min
    Remember-me30 days
    Consent1 year
    • ✅ Every custom cookie sets HttpOnly + Secure + SameSite, and no cookie exceeds its lifetime ceiling (session ≤ 120 min, remember-me ≤ 30 days, consent ≤ 1 year).
  4. Test the banner in incognito: only essential cookies pre-consent; “Accept All” loads analytics/marketing; “Reject” keeps them blocked; preferences are changeable later.

    • ✅ Pre-consent loads only essential cookies; Accept/Reject behave correctly; preferences are editable later.
  5. Use one consent mechanism only — if you use a third-party manager, disable the built-in one. Confirm the cookie policy page matches reality.

    • ✅ A single consent mechanism is active and the cookie policy page matches the real inventory.

Do not mark this step done until every box below is checked.

  • 👤 WCAG 2.1 AA passes — scanners clean; keyboard, focus, contrast, and structure verified by hand.
  • 🤖 Cookies inventoried — every cookie listed with attributes and a category.
  • 🤖 Session cookies secure — runtime config('session') has secure, http_only, same_site=lax.
  • 👤 Banner gates non-essential — incognito test confirms Accept/Reject and editable preferences.
  • 🔀 One consent mechanism — built-in disabled if a third-party manager is used; policy page matches.