diff --git a/composer.json b/composer.json index 30cd9a8..98a4f4d 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "require": { "php": "^8.1", "ext-json": "*", - "auth0/auth0-php": "^8.9", + "auth0/auth0-php": "^8.10", "illuminate/contracts": "^9 || ^10", "illuminate/http": "^9 || ^10", "illuminate/support": "^9 || ^10", @@ -56,7 +56,7 @@ "phpstan/phpstan-strict-rules": "^1", "psalm/plugin-laravel": "^2", "psr-mock/http": "^1", - "rector/rector": "0.17.6", + "rector/rector": "0.17.0", "squizlabs/php_codesniffer": "^3", "symfony/cache": "^6", "vimeo/psalm": "^5", @@ -112,20 +112,11 @@ } }, "scripts": { - "pest": [ - "@putenv XDEBUG_MODE=coverage", - "@php vendor/bin/pest --colors=always --strict-global-state --fail-on-risky --fail-on-warning --coverage --strict-coverage --compact" - ], - "pest:ci": [ - "@pest:fast --order-by=random --no-progress" - ], - "pest:fast": [ - "@pest --parallel" - ], - "phpcs": [ - "@putenv PHP_CS_FIXER_IGNORE_ENV=1", - "@php vendor/bin/php-cs-fixer fix --dry-run --diff" - ], + "pest": "@php vendor/bin/pest --order-by random --fail-on-risky --parallel --no-progress", + "pest:coverage": "@php vendor/bin/pest --order-by random --fail-on-risky --coverage --parallel --no-progress", + "pest:debug": "@php vendor/bin/pest --log-events-verbose-text pest.log --display-errors --fail-on-risky --no-progress", + "pest:profile": "@php vendor/bin/pest --profile", + "phpcs": "@php vendor/bin/php-cs-fixer fix --dry-run --diff", "phpcs:fix": "@php vendor/bin/php-cs-fixer fix", "phpstan": "@php vendor/bin/phpstan analyze", "psalm": "@php vendor/bin/psalm", diff --git a/config/auth0.php b/config/auth0.php index 9bb62d3..047d561 100644 --- a/config/auth0.php +++ b/config/auth0.php @@ -37,6 +37,8 @@ Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_KEY => Configuration::get(Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_KEY), Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_ALGORITHM => Configuration::get(Configuration::CONFIG_CLIENT_ASSERTION_SIGNING_ALGORITHM), Configuration::CONFIG_PUSHED_AUTHORIZATION_REQUEST => Configuration::get(Configuration::CONFIG_PUSHED_AUTHORIZATION_REQUEST), + Configuration::CONFIG_BACKCHANNEL_LOGOUT_CACHE => Configuration::get(Configuration::CONFIG_BACKCHANNEL_LOGOUT_CACHE), + Configuration::CONFIG_BACKCHANNEL_LOGOUT_EXPIRES => Configuration::get(Configuration::CONFIG_BACKCHANNEL_LOGOUT_EXPIRES), ], 'api' => [ diff --git a/docs/BackchannelLogout.md b/docs/BackchannelLogout.md new file mode 100644 index 0000000..6e109cd --- /dev/null +++ b/docs/BackchannelLogout.md @@ -0,0 +1,17 @@ +# Backchannel Logout + +The Auth0 Laravel SDK supports [Backchannel Logout](https://auth0.com/docs/authenticate/login/logout/back-channel-logout) from v7.12 onward. To use this feature, some additional configuration is necessary: + +1. **Add a new route to your application.** This route must be publicly accessible. Auth0 will use it to send backchannel logout requests to your application. For example: + +```php +Route::post('/backchannel', function (Request $request) { + if ($request->has('logout_token')) { + app('auth0')->handleBackchannelLogout($request->string('logout_token', '')->trim()); + } +}); +``` + +2. **Configure your Auth0 tenant to use Backchannel Logout.** See the [Auth0 documentation](https://auth0.com/docs/authenticate/login/logout/back-channel-logout/configure-back-channel-logout) for more information on how to do this. Please ensure you point the Logout URI to the backchannel route we just added to your application. + +Note: If your application's configuration assigns `false` to the `backchannelLogoutCache` SDK configuration property, this feature will be disabled entirely. diff --git a/docs/Configuration.md b/docs/Configuration.md index f8d7c79..8f9b587 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -85,6 +85,8 @@ The following environment variables are supported, but should not be adjusted un | `AUTH0_CLIENT_ASSERTION_SIGNING_KEY` | `String` The key to use for signing client assertions. | | `AUTH0_CLIENT_ASSERTION_SIGNING_ALGORITHM` | `String` The algorithm to use for signing client assertions. Defaults to `RS256`. | | `AUTH0_PUSHED_AUTHORIZATION_REQUEST` | `Boolean` Whether the SDK should use Pushed Authorization Requests during authentication. Note that your tenant must have this feature enabled. Defaults to `false`. | +| `AUTH0_BACKCHANNEL_LOGOUT_CACHE` | `String (class name)` A PSR-6 class to use for caching backchannel logout tokens. | +| `AUTH0_BACKCHANNEL_LOGOUT_EXPIRES` | `Integer` How long (in seconds) to cache a backchannel logout token. Defaults to `2592000` (30 days). | ### Order of Priority diff --git a/src/Configuration.php b/src/Configuration.php index ef7966f..b078dee 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -50,6 +50,7 @@ final class Configuration implements ConfigurationContract self::CONFIG_TOKEN_CACHE_TTL, self::CONFIG_HTTP_MAX_RETRIES, self::CONFIG_COOKIE_EXPIRES, + self::CONFIG_BACKCHANNEL_LOGOUT_EXPIRES, ]; /** @@ -57,6 +58,16 @@ final class Configuration implements ConfigurationContract */ public const CONFIG_AUDIENCE = 'audience'; + /** + * @var string + */ + public const CONFIG_BACKCHANNEL_LOGOUT_CACHE = 'backchannelLogoutCache'; + + /** + * @var string + */ + public const CONFIG_BACKCHANNEL_LOGOUT_EXPIRES = 'backchannelLogoutExpires'; + /** * @var string */ diff --git a/src/Entities/InstanceEntityAbstract.php b/src/Entities/InstanceEntityAbstract.php index e1f54f2..36d2929 100644 --- a/src/Entities/InstanceEntityAbstract.php +++ b/src/Entities/InstanceEntityAbstract.php @@ -29,6 +29,7 @@ public function __construct( protected ?CacheItemPoolInterface $tokenCachePool = null, protected ?CacheItemPoolInterface $managementTokenCachePool = null, protected ?string $guardConfigurationKey = null, + protected ?CacheItemPoolInterface $backchannelLogoutCachePool = null, ) { } @@ -119,6 +120,29 @@ abstract public function setConfiguration( SdkConfiguration | array | null $configuration = null, ): self; + protected function bootBackchannelLogoutCache(array $config): array + { + $backchannelLogoutCache = $config['backchannelLogoutCache'] ?? null; + + if (false === $backchannelLogoutCache) { + unset($config['backchannelLogoutCache']); + + return $config; + } + + if (null === $backchannelLogoutCache) { + $backchannelLogoutCache = $this->getBackchannelLogoutCachePool(); + } + + if (is_string($backchannelLogoutCache)) { + $backchannelLogoutCache = app(trim($backchannelLogoutCache)); + } + + $config['backchannelLogoutCache'] = $backchannelLogoutCache instanceof CacheItemPoolInterface ? $backchannelLogoutCache : null; + + return $config; + } + protected function bootManagementTokenCache(array $config): array { $managementTokenCache = $config['managementTokenCache'] ?? null; @@ -256,6 +280,15 @@ protected function createConfiguration( return $sdkConfiguration; } + protected function getBackchannelLogoutCachePool(): CacheItemPoolInterface + { + if (! $this->backchannelLogoutCachePool instanceof CacheItemPoolInterface) { + $this->backchannelLogoutCachePool = app(CacheBridge::class); + } + + return $this->backchannelLogoutCachePool; + } + protected function getManagementTokenCachePool(): CacheItemPoolInterface { if (! $this->managementTokenCachePool instanceof CacheItemPoolInterface) { diff --git a/tests/Unit/ServiceTest.php b/tests/Unit/ServiceTest.php index 9a5d0b1..6b5e677 100644 --- a/tests/Unit/ServiceTest.php +++ b/tests/Unit/ServiceTest.php @@ -188,6 +188,29 @@ ->tokenCache->toBeInstanceOf(CacheItemPoolInterface::class); }); +test('bootBackchannelLogoutCache() behaves as expected', function (): void { + $method = new ReflectionMethod(Service::class, 'bootBackchannelLogoutCache'); + $method->setAccessible(true); + + expect($method->invoke($this->laravel, [])) + ->backchannelLogoutCache->toBeInstanceOf(CacheBridgeContract::class); + + expect($method->invoke($this->laravel, ['backchannelLogoutCache' => null])) + ->backchannelLogoutCache->toBeInstanceOf(CacheBridgeContract::class); + + expect($method->invoke($this->laravel, ['backchannelLogoutCache' => CacheBridge::class])) + ->backchannelLogoutCache->toBeInstanceOf(CacheBridgeContract::class); + + expect($method->invoke($this->laravel, ['backchannelLogoutCache' => false])) + ->backchannelLogoutCache->toBeNull(); + + expect($method->invoke($this->laravel, ['backchannelLogoutCache' => MemoryStore::class])) + ->backchannelLogoutCache->toBeNull(); + + expect($method->invoke($this->laravel, ['backchannelLogoutCache' => 'cache.psr6'])) + ->backchannelLogoutCache->toBeInstanceOf(CacheItemPoolInterface::class); +}); + // test('bootManagementTokenCache() behaves as expected', function (): void { // $method = new ReflectionMethod(Service::class, 'bootManagementTokenCache'); // $method->setAccessible(true);