Skip to content

Commit

Permalink
feat: Add password broker handling to auth override (#67)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ollieread authored Nov 18, 2024
1 parent 2f67154 commit b4de25d
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 8 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand Down
160 changes: 160 additions & 0 deletions src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);

namespace Sprout\Overrides\Auth;

use Illuminate\Auth\Passwords\DatabaseTokenRepository;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use SensitiveParameter;
use Sprout\Exceptions\TenancyMissing;
use Sprout\Exceptions\TenantMissing;
use function Sprout\sprout;

/**
* Tenant Aware Database Token Repository
*
* This is a database token repository that wraps the default
* {@see \Illuminate\Auth\Passwords\DatabaseTokenRepository} to query based on
* the current tenant.
*
* @package Overrides
*/
class TenantAwareDatabaseTokenRepository extends DatabaseTokenRepository
{
/**
* Build the record payload for the table.
*
* @param string $email
* @param string $token
*
* @return array<string, mixed>
*
* @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']);
}
}
73 changes: 73 additions & 0 deletions src/Overrides/Auth/TenantAwarePasswordBrokerManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);

namespace Sprout\Overrides\Auth;

use Illuminate\Auth\Passwords\CacheTokenRepository;
use Illuminate\Auth\Passwords\PasswordBrokerManager;
use Illuminate\Auth\Passwords\TokenRepositoryInterface;

/**
* Tenant Aware Password Broker Manager
*
* This is an override of the default password broker manager to make it
* create a tenant-aware {@see \Illuminate\Auth\Passwords\TokenRepositoryInterface}.
*
* This is an unfortunate necessity as there's no other way to control the
* token repository that is created.
*
* @package Overrides
*/
class TenantAwarePasswordBrokerManager extends PasswordBrokerManager
{
/**
* Create a token repository instance based on the current configuration.
*
* @param array<string, mixed> $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;
}
}
63 changes: 62 additions & 1 deletion src/Overrides/AuthOverride.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,7 +21,7 @@
*
* @package Overrides
*/
final class AuthOverride implements ServiceOverride
final class AuthOverride implements ServiceOverride, BootableServiceOverride, DeferrableServiceOverride
{
/**
* @var \Illuminate\Auth\AuthManager
Expand All @@ -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
*
Expand All @@ -48,6 +91,7 @@ public function __construct(AuthManager $authManager)
public function setup(Tenancy $tenancy, Tenant $tenant): void
{
$this->forgetGuards();
$this->flushPasswordBrokers();
}

/**
Expand All @@ -69,6 +113,7 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void
public function cleanup(Tenancy $tenancy, Tenant $tenant): void
{
$this->forgetGuards();
$this->flushPasswordBrokers();
}

/**
Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@
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
* to the query to ensure sessions are tenanted.
*
* @package Overrides
*/
class DatabaseSessionHandler extends OriginalDatabaseSessionHandler
class TenantAwareDatabaseSessionHandler extends DatabaseSessionHandler
{
/**
* Get a fresh query builder instance for the table.
Expand Down
4 changes: 2 additions & 2 deletions src/Overrides/SessionOverride.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit b4de25d

Please sign in to comment.