From b4de25d803a33c2d8b3abbc40568fd22617b7bd4 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 11:03:17 +0000 Subject: [PATCH] feat: Add password broker handling to auth override (#67) * refactor: Rename the tenant aware session handler to avoid confusion * build(composer): Bump minimum Laravel version to include my supporting PRs * feat: Add password broker handling to auth override * chore: Make sure the token repository has a fallback when not in multitenanted context --- composer.json | 2 +- .../TenantAwareDatabaseTokenRepository.php | 160 ++++++++++++++++++ .../Auth/TenantAwarePasswordBrokerManager.php | 73 ++++++++ src/Overrides/AuthOverride.php | 63 ++++++- ... => TenantAwareDatabaseSessionHandler.php} | 7 +- src/Overrides/SessionOverride.php | 4 +- 6 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php create mode 100644 src/Overrides/Auth/TenantAwarePasswordBrokerManager.php rename src/Overrides/Session/{DatabaseSessionHandler.php => TenantAwareDatabaseSessionHandler.php} (85%) diff --git a/composer.json b/composer.json index 29192ba..013db51 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "type" : "library", "require" : { "php" : "^8.2", - "laravel/framework": "^11.0", + "laravel/framework": "^11.32", "league/flysystem-path-prefixing": "^3.0" }, "require-dev" : { diff --git a/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php b/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php new file mode 100644 index 0000000..76debed --- /dev/null +++ b/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php @@ -0,0 +1,160 @@ + + * + * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissing + */ + protected function getPayload($email, #[SensitiveParameter] $token): array + { + if (! sprout()->withinContext()) { + return parent::getPayload($email, $token); + } + + $tenancy = sprout()->getCurrentTenancy(); + + if ($tenancy === null) { + throw TenancyMissing::make(); + } + + if (! $tenancy->check()) { + throw TenantMissing::make($tenancy->getName()); + } + + return [ + 'tenancy' => $tenancy->getName(), + 'tenant_id' => $tenancy->key(), + 'email' => $email, + 'token' => $this->hasher->make($token), + 'created_at' => new Carbon(), + ]; + } + + /** + * Get the tenanted query + * + * @param string $email + * + * @return \Illuminate\Database\Query\Builder + * + * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissing + */ + protected function getTenantedQuery(string $email): Builder + { + if (! sprout()->withinContext()) { + return $this->getTable()->where('email', $email); + } + + $tenancy = sprout()->getCurrentTenancy(); + + if ($tenancy === null) { + throw TenancyMissing::make(); + } + + if (! $tenancy->check()) { + throw TenantMissing::make($tenancy->getName()); + } + + return $this->getTable() + ->where('tenancy', $tenancy->getName()) + ->where('tenant_id', $tenancy->key()) + ->where('email', $email); + } + + /** + * Get the record for a user + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * + * @return object|null + * + * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissing + */ + protected function getExistingTenantedRecord(CanResetPasswordContract $user): ?object + { + return $this->getTenantedQuery($user->getEmailForPasswordReset())->first(); + } + + /** + * Delete all existing reset tokens from the database. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * + * @return int + * + * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissing + */ + protected function deleteExisting(CanResetPasswordContract $user): int + { + return $this->getTenantedQuery($user->getEmailForPasswordReset())->delete(); + } + + /** + * Determine if a token record exists and is valid. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * @param string $token + * + * @return bool + * + * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissing + */ + public function exists(CanResetPasswordContract $user, #[SensitiveParameter] $token): bool + { + $record = (array)$this->getExistingTenantedRecord($user); + + return $record && + ! $this->tokenExpired($record['created_at']) && + $this->hasher->check($token, $record['token']); + } + + /** + * Determine if the given user recently created a password reset token. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * + * @return bool + * + * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissing + */ + public function recentlyCreatedToken(CanResetPasswordContract $user): bool + { + $record = (array)$this->getExistingTenantedRecord($user); + + return $record && $this->tokenRecentlyCreated($record['created_at']); + } +} diff --git a/src/Overrides/Auth/TenantAwarePasswordBrokerManager.php b/src/Overrides/Auth/TenantAwarePasswordBrokerManager.php new file mode 100644 index 0000000..91c83a3 --- /dev/null +++ b/src/Overrides/Auth/TenantAwarePasswordBrokerManager.php @@ -0,0 +1,73 @@ + $config + * + * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface + */ + protected function createTokenRepository(array $config): TokenRepositoryInterface + { + // @phpstan-ignore-next-line + $key = $this->app['config']['app.key']; + + if (str_starts_with($key, 'base64:')) { + $key = base64_decode(substr($key, 7)); + } + + if (isset($config['driver']) && $config['driver'] === 'cache') { + return new CacheTokenRepository( + $this->app['cache']->store($config['store'] ?? null), // @phpstan-ignore-line + $this->app['hash'], // @phpstan-ignore-line + $key, + ($config['expire'] ?? 60) * 60, + $config['throttle'] ?? 0, // @phpstan-ignore-line + $config['prefix'] ?? '', // @phpstan-ignore-line + ); + } + + $connection = $config['connection'] ?? null; + + return new TenantAwareDatabaseTokenRepository( + $this->app['db']->connection($connection), // @phpstan-ignore-line + $this->app['hash'], // @phpstan-ignore-line + $config['table'], // @phpstan-ignore-line + $key, + $config['expire'],// @phpstan-ignore-line + $config['throttle'] ?? 0// @phpstan-ignore-line + ); + } + + /** + * Flush the resolved brokers + * + * @return $this + */ + public function flush(): self + { + $this->brokers = []; + + return $this; + } +} diff --git a/src/Overrides/AuthOverride.php b/src/Overrides/AuthOverride.php index ab2cb6a..c7d41da 100644 --- a/src/Overrides/AuthOverride.php +++ b/src/Overrides/AuthOverride.php @@ -4,9 +4,14 @@ namespace Sprout\Overrides; use Illuminate\Auth\AuthManager; +use Illuminate\Contracts\Foundation\Application; +use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +use Sprout\Overrides\Auth\TenantAwarePasswordBrokerManager; +use Sprout\Sprout; /** * Auth Override @@ -16,7 +21,7 @@ * * @package Overrides */ -final class AuthOverride implements ServiceOverride +final class AuthOverride implements ServiceOverride, BootableServiceOverride, DeferrableServiceOverride { /** * @var \Illuminate\Auth\AuthManager @@ -33,6 +38,44 @@ public function __construct(AuthManager $authManager) $this->authManager = $authManager; } + /** + * Get the service to watch for before overriding + * + * @return string + */ + public static function service(): string + { + return AuthManager::class; + } + + /** + * Boot a service override + * + * This method should perform any initial steps required for the service + * override that take place during the booting of the framework. + * + * @param \Illuminate\Contracts\Foundation\Application&\Illuminate\Foundation\Application $app + * @param \Sprout\Sprout $sprout + * + * @return void + */ + public function boot(Application $app, Sprout $sprout): void + { + // Although this isn't strictly necessary, this is here to tidy up + // the list of deferred services, just in case there's some weird gotcha + // somewhere that causes the provider to be loaded anyway. + // We'll remove the two services we're about to bind against. + $app->removeDeferredServices(['auth.password', 'auth.password.broker']); + + // This is the actual thing we need. + $app->singleton('auth.password', function ($app) { + return new TenantAwarePasswordBrokerManager($app); + }); + + // I would ideally also like to mark the password reset service provider + // as loaded here, but that method is protected. + } + /** * Set up the service override * @@ -48,6 +91,7 @@ public function __construct(AuthManager $authManager) public function setup(Tenancy $tenancy, Tenant $tenant): void { $this->forgetGuards(); + $this->flushPasswordBrokers(); } /** @@ -69,6 +113,7 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void public function cleanup(Tenancy $tenancy, Tenant $tenant): void { $this->forgetGuards(); + $this->flushPasswordBrokers(); } /** @@ -82,4 +127,20 @@ private function forgetGuards(): void $this->authManager->forgetGuards(); } } + + /** + * Flush all password brokers + * + * @return void + */ + private function flushPasswordBrokers(): void + { + /** @var \Illuminate\Auth\Passwords\PasswordBrokerManager $passwordBroker */ + $passwordBroker = app('auth.password'); + + // The flush method only exists on our custom implementation + if ($passwordBroker instanceof TenantAwarePasswordBrokerManager) { + $passwordBroker->flush(); + } + } } diff --git a/src/Overrides/Session/DatabaseSessionHandler.php b/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php similarity index 85% rename from src/Overrides/Session/DatabaseSessionHandler.php rename to src/Overrides/Session/TenantAwareDatabaseSessionHandler.php index d8e9f42..a967f15 100644 --- a/src/Overrides/Session/DatabaseSessionHandler.php +++ b/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php @@ -4,14 +4,13 @@ namespace Sprout\Overrides\Session; use Illuminate\Database\Query\Builder; -use Illuminate\Session\DatabaseSessionHandler as OriginalDatabaseSessionHandler; -use RuntimeException; +use Illuminate\Session\DatabaseSessionHandler; use Sprout\Exceptions\TenancyMissing; use Sprout\Exceptions\TenantMissing; use function Sprout\sprout; /** - * Database Session Handler + * Tenant Aware Database Session Handler * * This is a database session driver that wraps the default * {@see \Illuminate\Session\DatabaseSessionHandler} and adds a where clause @@ -19,7 +18,7 @@ * * @package Overrides */ -class DatabaseSessionHandler extends OriginalDatabaseSessionHandler +class TenantAwareDatabaseSessionHandler extends DatabaseSessionHandler { /** * Get a fresh query builder instance for the table. diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index f56332a..ef9df10 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -17,7 +17,7 @@ use Sprout\Exceptions\MisconfigurationException; use Sprout\Exceptions\TenancyMissing; use Sprout\Exceptions\TenantMissing; -use Sprout\Overrides\Session\DatabaseSessionHandler; +use Sprout\Overrides\Session\TenantAwareDatabaseSessionHandler; use Sprout\Sprout; use function Sprout\sprout; @@ -220,7 +220,7 @@ private static function createDatabaseDriver(): Closure */ if (sprout()->withinContext()) { - return new DatabaseSessionHandler( + return new TenantAwareDatabaseSessionHandler( app()->make('db')->connection($connection), $table, $lifetime,