agecheck / php
PHP verification library for AgeCheck age-verification tokens
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/agecheck/php
Requires
- php: ^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5
- firebase/php-jwt: ^7.0.2
Requires (Dev)
- phpunit/phpunit: ^10.5.63
README
Server-side SDK for AgeCheck gate policy and JWT verification.
Features
- Verify AgeCheck JWTs signed with ES256
- Deployment mode: production or demo
- Enforce minimum age tier (
N+, not capped at21+) - Require session binding (
vc.credentialSubject.session) - Raise gate from edge header (
X-Age-Gate: true) in production, or always gate in demo deployment mode - Create and verify signed verification cookies
- Resolve verification keys from deployment-mode JWKS (
agecheck.mefor production,demo.agecheck.mefor demo) - Cache JWKS with TTL and stale-cache fallback
Install
composer require agecheck/php
Requirements:
- PHP
8.1+
Quickstart: see /Quickstart.md.
Core usage
<?php declare(strict_types=1); use AgeCheck\Config; use AgeCheck\Gate; use AgeCheck\Verifier; $config = new Config([ 'hmacSecret' => 'YOUR_32_BYTE_SECRET', 'deploymentMode' => Config::DEPLOYMENT_PRODUCTION, // production | demo 'requiredAge' => 18, 'cookieTtl' => 86400, // seconds; hostmaster-controlled (e.g. 31536000 for 1 year) // Defaults are pinned to AgeCheck issuer/JWKS unless explicitly opted in. 'allowCustomIssuer' => false, 'gateHeaderName' => 'X-Age-Gate', 'gateHeaderRequiredValue' => 'true', ]); $gate = new Gate($config); $verifier = new Verifier($config); if ($gate->isGateRequired()) { $result = $verifier->verify($jwt); if (!$result->isOk()) { // deny } }
Easy AgeGate Option
You can render gate HTML with either:
easyAgeGate: trueusingeasy-agegate.min.js, oreasyAgeGate: falseusing plainagegate.min.js(full custom UI flow)
<?php declare(strict_types=1); use AgeCheck\Gate; $gate = new Gate($config); echo $gate->renderGatePage([ 'redirect' => '/protected', 'easyAgeGate' => true, 'easyAgeGateOptions' => [ 'title' => 'Age Restricted Content', 'subtitle' => 'Please confirm your age anonymously using AgeCheck.me.', 'verifyButtonText' => 'Verify Now', 'logoUrl' => 'https://your-cdn/logo.svg', // optional ], ]);
Cookie helpers
<?php declare(strict_types=1); use AgeCheck\Gate; $gate = new Gate($config); if ($result->isOk() && is_array($result->claims())) { $gate->markVerified($result->claims()); }
Use Gate::isVerified() on protected routes to validate the signed cookie.
Signed cookie payload is minimal and stateless:
{ "verified": true, "exp": 1700000000, "level": "18+" }
You can also set the cookie through a provider-agnostic assertion boundary:
use AgeCheck\VerificationAssertion; $assertion = VerificationAssertion::verified('agecheck', '18+', time(), 'passkey'); $gate->markVerifiedFromVerificationAssertion($assertion);
Provider integration
Hostmasters can run multiple providers side-by-side and still keep one cookie/session contract, matching the Node SDK provider-agnostic pattern.
use AgeCheck\Provider; $expectedSession = $body['payload']['agegateway_session'] ?? null; if (!is_string($expectedSession) || $expectedSession === '') { // deny } if (($body['provider'] ?? 'agecheck') === 'agecheck') { $normalized = Provider::verifyAgeCheckCredential($verifier, $body['jwt'] ?? '', $expectedSession); } else { $external = $providerService->verify($body); $normalized = Provider::normalizeExternalProviderAssertion($external, $expectedSession); } if (($normalized['verified'] ?? null) !== true) { // deny (see $normalized['code']) } Provider::applyProviderAssertionCookie($gate, $normalized);
All providers converge to one assertion boundary (provider, verified, level, session, verifiedAtUnix), which keeps cookie issuance and protected-route enforcement consistent.
Session rules:
payload.agegateway_sessionis required- session must be a UUID
- provider assertion
sessionmust matchpayload.agegateway_session
Provider metadata fields (optional):
verificationType:passkey | oid4vp | otherevidenceType:webauthn_assertion | sd_jwt | zk_attestation | otherproviderTransactionId: provider transaction/reference idloa: level of assurance string
Security notes
- Backend enforcement remains authoritative; browser callbacks alone are not trusted.
- Require session binding in verification (
payload.agegateway_sessionmust equalvc.credentialSubject.session). - Use edge policy to set
X-Age-Gate: truewhere gate is legally required. - Use HTTPS JWKS only. Defaults are mode-specific:
- production:
https://agecheck.me/.well-known/jwks.json - demo:
https://demo.agecheck.me/.well-known/jwks.jsonwith production JWKS fallback for mixed demo/prod acceptance
- production:
- Custom issuer/JWKS overrides are disabled by default. Enable with
allowCustomIssuer=trueonly when intentional.
Standardized error codes
Verifier and provider helpers emit stable error codes such as:
invalid_inputinvalid_issuerinvalid_credentialinvalid_age_tierinsufficient_age_tiersession_binding_requiredsession_binding_mismatchtoken_expiredtoken_not_yet_validinvalid_signatureunknown_key_idverify_failed
Troubleshooting
If verify responses include:
{
"verified": false,
"code": "verify_failed",
"error": "Failed to issue verification cookie"
}
common causes are:
hmacSecretmissing in runtime config or shorter than 32 bytes- malformed verification assertion level (must be
N+, for example18+) - custom endpoint logic bypassing
Gate/Providerhelpers for cookie issuance
Examples
See examples/:
examples/protected_index.phpexamples/agecheck_gate.phpexamples/ageverify_api.phpexamples/provider_verify_api.phpexamples/session_api.phpexamples/session_reset_api.php
examples/protected_index.php mirrors the Node reference behavior:
- server-side gate enforcement
- restricted page rendering only after cookie validation
- signed-cookie TTL countdown and reset action
License
Apache-2.0