Server-side SDK for AgeCheck gate policy and JWT verification.
- 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
composer require agecheck/phpRequirements:
- PHP
8.1+
Quickstart: see /Quickstart.md.
<?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
}
}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
],
]);<?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);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
- 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.
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
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
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
Apache-2.0