xzawed / keycloak-sdk
Keycloak SDK for PHP — OIDC/OAuth2 authentication + Admin REST API, part of a nine-language polyglot SDK
Requires
- php: ^8.3
- firebase/php-jwt: ^7.1
- fschmtt/keycloak-rest-api-client-php: 0.42.0
- guzzlehttp/guzzle: ^7.9
- guzzlehttp/psr7: ^2.7
- league/oauth2-client: ^2.8
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- stevenmaguire/oauth2-keycloak: ^6.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.2
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^12
- roave/security-advisories: dev-latest
- testcontainers/testcontainers: ^1.0
This package is auto-updated.
Last update: 2026-08-01 16:57:40 UTC
README
An idiomatic PHP SDK for Keycloak covering both OIDC/OAuth2 authentication and the Admin REST API behind one consistent facade.
Part of a nine-language polyglot SDK (Java · Python · Node · Go · C# · PHP · Rust · Ruby · Kotlin) — one API shape, nine idioms: github.com/xzawed/KeyCloakSDK.
Pre-release — not yet published to Packagist.
Requirements
- PHP 8.3+ (
composer.jsonrequires^8.3) - Keycloak server 26.6.x (verified by the integration suite)
Install
The SDK is developed in the php/ directory of a polyglot monorepo, and Packagist cannot install from a subdirectory. Releases are therefore subtree-split into the dedicated read-only repository xzawed/keycloak-sdk-php, which is what Packagist reads — the package name stays xzawed/keycloak-sdk:
composer require xzawed/keycloak-sdk
use Xzawed\Keycloak\{KeycloakClient, KeycloakConfig}; // admin lives under Xzawed\Keycloak\Admin
Quickstart
KeycloakClient::create() assembles auth immediately (no network); admin() is created lazily on first call and needs a client secret. Value types are final readonly class, and failures throw the KeycloakException hierarchy.
<?php declare(strict_types=1); require __DIR__ . '/vendor/autoload.php'; use Fschmtt\Keycloak\Representation\User; use Xzawed\Keycloak\KeycloakClient; use Xzawed\Keycloak\KeycloakConfig; $client = KeycloakClient::create(new KeycloakConfig( serverUrl: 'https://kc.example.com', realm: 'myrealm', clientId: 'my-app', clientSecret: '…', // load from an env var / secret manager; __toString is auto-masked )); // 1) client-credentials grant. TokenSet::__toString() masks the tokens (accessToken=***). $token = $client->auth()->clientCredentialsToken(); echo "token type: {$token->tokenType}, expires in: {$token->expiresIn}s\n"; // 2) hardened verification (alg pinning · exact iss · aud containment · mandatory exp · clock skew). $validated = $client->auth()->validate($token->accessToken); echo "subject: {$validated->subject}, issuer: {$validated->issuer}\n"; // 3) admin API — create returns void, so look the id up afterwards with findIdByUsername(). $client->admin()->users()->create(new User(username: 'alice', enabled: true)); $userId = $client->admin()->users()->findIdByUsername('alice'); echo "created userId={$userId}\n";
Audience: validation requires the token's
audto containclientId. A stock realm does not put the client id in a client-credentials token'saud, so on a default realm either passexpectedAudience: 'my-api'(the audience your realm actually issues), or add an Audience protocol mapper to the client in Keycloak.
Admin failures surface as KeycloakNotFoundError / KeycloakConflictError / KeycloakForbiddenError (all carrying KeycloakAdminError::getStatusCode()), network failures as KeycloakTransportError. admin()->raw() is the escape hatch to the underlying typed client.
Security defaults
- Algorithm pinning — the accepted JWT signature algorithms are pinned (
RS256by default, configurable viasignatureAlgorithms:); the header-suppliedalg, includingnone, is never trusted. The SDK decodes the raw header segment itself to gate onalgbefore verification, becausefirebase/php-jwtonly fills its&$headersout-parameter after a successful decode. - Hardened claims — exact
issmatch,audcontainment check, mandatoryexp(a token without one is rejected), and a bounded clock skew (clockSkew:, default 30s). - DoS-safe JWKS — a refetch is triggered only by an unresolved key ID (rotation) and never by a bad signature, and is rate-limited by
jwksMinRefetchSeconds:(default 30s) — so no volume of forged randomkids makes the SDK issue more than one JWKS request per interval. - Secret handling —
KeycloakConfigandTokenSetmask secrets and tokens fully (***, no prefix) in their__toString(); TLS verification is on by default and both connect and read timeouts are always applied.
Two scope limits worth knowing. The JWKS cache and its rate limit are per-JwksStore in-memory state, so their reach follows your deployment model: under a long-running worker (Swoole, RoadRunner) they span requests, but under classic PHP-FPM every request builds a fresh store and the limit only binds within that one request. And masking covers this SDK's own __toString() — PHP has no erasable string type, so the client secret lives in an ordinary string for its lifetime and masking is defence in depth, not a guarantee about your logs.
Versioning and support
This SDK is pre-1.0. Under SemVer a 0.x minor bump may carry breaking changes, so read the release notes before upgrading. Only the newest released version of each language SDK receives security fixes — there are no LTS lines, and older 0.x releases are not backported to. Full policy: SECURITY.md.
Documentation
- Getting started — per-language install, quickstart, and compatibility matrix
- Full PHP example
- Deploying a Keycloak server
- Security policy