Loading...
Loading...
Use when protecting PHP API endpoints with JWT Bearer token validation or scope checks. Integrates auth0/auth0-php in API mode for stateless APIs receiving access tokens.
npx skill4agent add auth0/agent-skills auth0-php-apiauth0/auth0-phpSTRATEGY_APImbstringopenssljsonauth0-quickstartauth0-phpauth0/laravel-auth0auth0/symfonyauth0-reactauth0-vueauth0-angularcomposer require auth0/auth0-php vlucas/phpdotenv guzzlehttp/guzzle guzzlehttp/psr7 "symfony/cache:^7.0"auth0/auth0-phpvlucas/phpdotenv.env$_ENVguzzlehttp/guzzleguzzlehttp/psr7symfony/cacheSTOP - ask the user before proceeding.Ask exactly this question and wait for their answer before doing anything else:"How would you like to create the Auth0 API resource?
- Automated - I'll run Auth0 CLI scripts that create the resource and write the exact values to your
automatically..env- Manual - You create the API yourself in the Auth0 Dashboard (or via
) and provide me the Domain and Audience.auth0 apis createWhich do you prefer? (1 = Automated / 2 = Manual)"Do NOT proceed to any setup steps until the user has answered. Do NOT default to manual.
.env# Using Auth0 CLI
auth0 apis create \
--name "My PHP API" \
--identifier https://my-api.example.com \
--json.envAUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_AUDIENCE=https://your-api.example.comAUTH0_DOMAINhttps://AUTH0_AUDIENCEauth0.php<?php
require 'vendor/autoload.php';
use Auth0\SDK\Auth0;
use Auth0\SDK\Configuration\SdkConfiguration;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$configuration = new SdkConfiguration(
strategy: SdkConfiguration::STRATEGY_API,
domain: $_ENV['AUTH0_DOMAIN'],
clientId: null,
audience: [$_ENV['AUTH0_AUDIENCE']],
tokenAlgorithm: 'RS256',
tokenCache: new FilesystemAdapter('auth0_jwks', 600, __DIR__ . '/var/cache'),
tokenCacheTtl: 600,
);
$auth0 = new Auth0($configuration);STRATEGY_APIclientIdaudiencetokenCacheCacheItemPoolInterfacemiddleware.php<?php
use Auth0\SDK\Auth0;
use Auth0\SDK\Token;
use Auth0\SDK\Exception\InvalidTokenException;
function requireAuth(Auth0 $auth0, ?array $requiredScopes = null): array
{
$token = $auth0->getBearerToken(
server: ['HTTP_AUTHORIZATION']
);
if ($token === null) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'unauthorized', 'message' => 'Missing or invalid Bearer token']);
exit;
}
$claims = $token->toArray();
if ($requiredScopes !== null) {
$grantedScopes = isset($claims['scope']) ? explode(' ', $claims['scope']) : [];
$missingScopes = array_diff($requiredScopes, $grantedScopes);
if (!empty($missingScopes)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'insufficient_scope', 'message' => 'Token lacks required scopes']);
exit;
}
}
return $claims;
}getBearerToken()server$_SERVER['HTTP_AUTHORIZATION']$_SERVERTokenInterfacenullindex.php<?php
require 'auth0.php';
require 'middleware.php';
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
header('Content-Type: application/json');
switch ($path) {
case '/api/public':
echo json_encode(['message' => 'Public endpoint - no authentication required']);
break;
case '/api/private':
$claims = requireAuth($auth0);
echo json_encode(['message' => 'Private endpoint', 'sub' => $claims['sub']]);
break;
case '/api/private-scoped':
$claims = requireAuth($auth0, ['read:messages']);
echo json_encode(['messages' => [], 'sub' => $claims['sub']]);
break;
default:
http_response_code(404);
echo json_encode(['error' => 'not_found']);
break;
}$claims = requireAuth($auth0);
$userId = $claims['sub']; // user/client ID
$scopes = $claims['scope']; // space-separated granted scopes
$issuer = $claims['iss']; // issuer (your Auth0 domain URL)
$audience = $claims['aud']; // audience (string or array)
$expiration = $claims['exp']; // expiration timestampToken$token = $auth0->getBearerToken(server: ['HTTP_AUTHORIZATION']);
if ($token !== null) {
$subject = $token->getSubject(); // returns ?string
$issuer = $token->getIssuer(); // returns ?string
$audience = $token->getAudience(); // returns ?array
$expiration = $token->getExpiration(); // returns ?int
}cors.php<?php
function handleCors(array $allowedOrigins): void
{
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}index.phprequire 'auth0.php'require 'cors.php';
handleCors(['https://your-spa-domain.com']);index.php<?php
require 'cors.php';
handleCors(['https://your-spa-domain.com']);
require 'auth0.php';
require 'middleware.php';
// ... rest of index.phpOPTIONSAgent instruction: Start the PHP built-in server and verify the basic endpoints work without credentials:bashphp -S localhost:8000 index.php & sleep 1 curl -s http://localhost:8000/api/public curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/private
should return/api/public{"message":"Public endpoint - no authentication required"} should return HTTP 401/api/privateIf both pass, the middleware is working. Kill the background server when done:bashkill %1 2>/dev/nullFailcheck: If the server fails to start orerrors, check for PHP syntax issues or missing/api/public. After 5-6 failed iterations, use.envto ask the user for more details about their environment.AskUserQuestion
Agent instruction: STOP - ask after basic tests pass.Once public/401 tests pass, ask the user:"Your API is set up and the middleware is working (public returns 200, private returns 401 without a token). Would you like me to help you set up testing with a real access token?
- Yes - I'll help you configure an M2M application to get test tokens.
- No - I'm done for now.
Which do you prefer?"Do NOT proceed with M2M/token setup unless the user says yes. If they say no, summarize what was done and stop.
Agent instruction (M2M app setup - only if user wants testing):If the user chose to set up testing, ask:"To test your protected endpoints, you need a Machine-to-Machine (M2M) application authorized to request tokens for this API.
- Create new - I'll create a new M2M application and authorize it for this API.
- Use existing - You already have an M2M application. Provide the Client ID and I'll authorize it for this API.
Which do you prefer? (1 = Create new / 2 = Use existing)"Do NOT proceed until the user answers. Do NOT silently pick an existing application from the tenant.If the user chose "Create new":bashauth0 apps create \ --name "${PWD##*/} (Test App)" \ --type m2m \ --no-input --jsonParse the JSON withto extractjq. Do NOT useclient_id- never expose client secrets in agent context. Then create a client grant:--reveal-secretsbashauth0 api post "client-grants" --data '{ "client_id": "<CLIENT_ID>", "audience": "<API_IDENTIFIER>", "scope": ["<SCOPES>"] }'If the user chose "Use existing": Ask for the Client ID. Then create a client grant to authorize it for this API:bashauth0 api post "client-grants" --data '{ "client_id": "<USER_PROVIDED_CLIENT_ID>", "audience": "<API_IDENTIFIER>", "scope": ["<SCOPES>"] }'If the grant already exists (409 conflict), that's fine - the app is already authorized.
Agent instruction (TOKEN ISOLATION - CRITICAL):The agent MUST NEVER directly see or display access token values. Token security rules:
- Do NOT run
on its own - it outputs the token to stdoutauth0 test token- Do NOT ask the user to paste their token into the conversation
- Do NOT echo, print, or log the token value
- Do NOT store the token in a file
Secure testing approach (single-command chain):If the user explicitly asks to test the protected endpoints, use a single-command chain that captures the token into a shell variable and immediately uses it:bashphp -S localhost:8000 index.php & sleep 1 TEST_TOKEN=$(auth0 test token <M2M_CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> 2>/dev/null | grep -o 'ey[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*') && \ [ -n "$TEST_TOKEN" ] && echo "Token acquired (${#TEST_TOKEN} chars)" && \ echo "=== GET /api/private ===" && \ curl -s http://localhost:8000/api/private -H "Authorization: Bearer $TEST_TOKEN" && \ echo "" && echo "=== GET /api/private-scoped ===" && \ curl -s http://localhost:8000/api/private-scoped -H "Authorization: Bearer $TEST_TOKEN" kill %1 2>/dev/nullRules:
- ONLY use when the user explicitly asks to test
- Always chain token acquisition + curl in a SINGLE
command&&- NEVER add
or any command that would print the raw token valueecho $TEST_TOKEN- If the token acquisition fails (empty variable), report that the M2M app may not be authorized
- Client ID is REQUIRED - if M2M setup was not completed, ask the user first
If the user does NOT ask to test, just provide the commands for them to run manually:auth0 test token <CLIENT_ID> --audience <AUDIENCE> --scopes <SCOPE1,SCOPE2> curl http://localhost:8000/api/private -H "Authorization: Bearer <PASTE_TOKEN_HERE>"
php -S localhost:8000 index.phpcurl http://localhost:8000/api/publiccurl http://localhost:8000/api/privatecurl http://localhost:8000/api/private \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"curl http://localhost:8000/api/private-scoped \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"| Mistake | Fix |
|---|---|
Hardcoding | Always read from environment variables - never embed credentials in code |
Using | API mode must use |
| Installing without a PSR-18 HTTP client | Must have |
| Not caching JWKS keys | Without a PSR-6 cache, the SDK fetches JWKS on every request - always configure |
Passing | |
Passing | |
Using | Always pass |
| Echoing exception messages to users | Use |
| Using an ID token instead of an access token | Must use the access token for API auth - ID tokens are for the client app |
| Created an Application instead of an API in Auth0 | Must create an API resource (Applications -> APIs) - an Application doesn't issue access tokens with the right audience |
Setting | For RS256, |
Using | |
Passing | The |
| Method | Returns | Purpose |
|---|---|---|
| | Searches specified |
| | Manually decodes and validates a JWT string |
| | Access the SDK configuration instance |
| | Returns all token claims as an associative array |
| | Returns the |
| | Returns the |
| | Returns the |
| | Returns the |
auth0-phpauth0-quickstartauth0-cliauth0-mfa$configuration = new SdkConfiguration(
strategy: SdkConfiguration::STRATEGY_API, // required - stateless mode
domain: $_ENV['AUTH0_DOMAIN'], // required
audience: [$_ENV['AUTH0_AUDIENCE']], // required - array of identifiers
tokenAlgorithm: 'RS256', // default
tokenCache: $psrCacheAdapter, // recommended for production
tokenCacheTtl: 600, // JWKS cache TTL in seconds
);$token = $auth0->getBearerToken(server: ['HTTP_AUTHORIZATION']); // returns ?TokenInterface
$claims = $token->toArray(); // all claims as array
$userId = $token->getSubject(); // sub claimuse Auth0\SDK\Token;
$token = $auth0->decode(
$jwtString,
tokenType: Token::TYPE_ACCESS_TOKEN,
);AUTH0_DOMAINtenant.us.auth0.comAUTH0_AUDIENCEhttps://api.example.comrequireAuth($auth0)requireAuth($auth0, ['read:messages'])