christianjbrown / oauth2-client
A thin, strongly-typed PHP 8.5+ OAuth 2.0 client that manages access tokens (refresh-token and client-credentials grants), caching them behind a mockable key-value store.
Package info
github.com/christianjbrown/oauth2-client-php
pkg:composer/christianjbrown/oauth2-client
Requires
- php: ^8.5
- christianjbrown/api-client: ^1.0 || ^2.0 || ^3.0
- christianjbrown/key-value-store: ^1.0 || ^2.0 || ^3.0
- psr/clock: ^1.0
- psr/http-client: ^1.0
- symfony/clock: ^7.3 || ^8.0
Requires (Dev)
- christianjbrown/code-quality-scripts: ^1.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-01 13:35:39 UTC
README
A small, strongly-typed PHP OAuth 2.0 client that fetches and caches access tokens. It hides the token endpoint behind a couple of token managers, caches the resulting access (and refresh) token in an interchangeable key-value store, and only calls the endpoint again when the cached token is missing, expired, or a refresh is forced.
Two grant types ship today:
- Refresh token (
RefreshTokenManager) — exchanges a stored refresh token for a new access token. - Client credentials (
ClientCredentialsTokenManager) — exchanges HTTP Basic credentials for an access token.
Both return an AccessTokenInterface and normalise transport and payload failures into a single
library exception hierarchy, so callers stay decoupled from the underlying HTTP client.
✔️ Prerequisites
💡 If you're on MacOS and have Homebrew, PHP and Composer will install with brew install composer.
🏗️ Installation
For your composer-enabled project:
composer require christianjbrown/oauth2-client
💻 Usage
Build a manager with its factory. Each factory takes a PSR-20 clock (these examples use
Symfony\Component\Clock\NativeClock) and builds the manager from a JSON API request sender (from
api-client), one or more key-value
stores for the cached tokens, the token endpoint URL and a lock. Pass NullLock when only one
process refreshes tokens at a time.
🔄 Refresh token grant
use ChristianBrown\OAuth2Client\Authentication\PublicClientAuthentication; use ChristianBrown\OAuth2Client\Lock\NullLock; use ChristianBrown\OAuth2Client\RefreshTokenManagerFactory; use Symfony\Component\Clock\NativeClock; $manager = (new RefreshTokenManagerFactory(new NativeClock()))->create( $jsonApiRequestSender, // ChristianBrown\ApiClient\JsonApiRequestSenderInterface $accessTokenStore, // ChristianBrown\KeyValueStore\TtlAwareKeyValueStoreInterface $refreshTokenStore, // ChristianBrown\KeyValueStore\KeyValueStoreInterface 'https://example.com/oauth/token', new PublicClientAuthentication(), // or new ClientSecretBasicAuthentication('my-client-secret') new NullLock(), // or your own LockInterface implementation ); $accessToken = $manager->getAccessToken('my-client-id'); $accessToken->getAccessToken(); // the bearer token string $accessToken->getExpiresIn(); // seconds until expiry // Force a refresh even if a valid token is cached: $accessToken = $manager->getAccessToken('my-client-id', true);
The manager returns the cached access token while it is still valid. Otherwise it POSTs the stored refresh token to the endpoint, caches the new access and refresh tokens, and returns the fresh token.
🔑 Client credentials grant
use ChristianBrown\OAuth2Client\ClientCredentialsTokenManagerFactory; use ChristianBrown\OAuth2Client\Lock\NullLock; use Symfony\Component\Clock\NativeClock; $manager = (new ClientCredentialsTokenManagerFactory(new NativeClock()))->create( $jsonApiRequestSender, // ChristianBrown\ApiClient\JsonApiRequestSenderInterface $accessTokenStore, // ChristianBrown\KeyValueStore\TtlAwareKeyValueStoreInterface 'https://example.com/oauth/token', new NullLock(), ); // The Basic auth value is the raw "client_id:client_secret"; the manager base64-encodes it. $accessToken = $manager->getAccessTokenFromBasicAuth( 'my-client-id:my-client-secret', 'my-scope', // optional 'my-client-id', // optional ); $accessToken->getAccessToken();
⬆️ Upgrading to 2.0
The manager constructors now take their collaborators, so build managers with the factories. AccessTokenTransformer
and LockInterface are unchanged.
// Before $manager = new RefreshTokenManager($sender, $accessStore, $refreshStore, new AccessTokenTransformer(), $url, $clientSecret, $lock); $manager = new ClientCredentialsTokenManager($sender, $accessStore, new AccessTokenTransformer(), $url); // After $factory = new RefreshTokenManagerFactory(new NativeClock()); $manager = $factory->create($sender, $accessStore, $refreshStore, $url, new ClientSecretBasicAuthentication($clientSecret), $lock); $manager = (new ClientCredentialsTokenManagerFactory(new NativeClock()))->create($sender, $accessStore, $url, new NullLock());
Without a client secret use new PublicClientAuthentication(); without a lock use new NullLock().
🎫 The access token
Every manager returns a ChristianBrown\OAuth2Client\Model\AccessTokenInterface:
public function getAccessToken(): string; public function getExpiresIn(): int; public function getRefreshToken(): ?string; public function getScope(): ?string; public function getTokenType(): AccessTokenType; // enum, currently AccessTokenType::BEARER
🚨 Error handling
Everything the library throws implements
ChristianBrown\OAuth2Client\Model\Exception\ExceptionInterface (which extends Throwable):
RequestExceptionInterface— the token endpoint request failed. The originalapi-clientexception is available viagetRequestException().BadResponsePayloadFieldExceptionInterface— the endpoint responded, but a field was missing, the wrong type, or an unsupported value.getField()andgetData()expose the offending field and the full payload.
use ChristianBrown\OAuth2Client\Model\Exception\ExceptionInterface; try { $accessToken = $manager->getAccessToken('my-client-id'); } catch (ExceptionInterface $e) { print $e->getMessage(); }
📝 Changelog
Notable changes in each release are listed in CHANGELOG.md.
📄 License
Released under the MIT License.