diff --git a/src/Bridge/ClientRepository.php b/src/Bridge/ClientRepository.php index 9cd17762e..204e6e270 100644 --- a/src/Bridge/ClientRepository.php +++ b/src/Bridge/ClientRepository.php @@ -3,6 +3,7 @@ namespace Laravel\Passport\Bridge; use Laravel\Passport\ClientRepository as ClientModelRepository; +use Laravel\Passport\Passport; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; class ClientRepository implements ClientRepositoryInterface @@ -55,7 +56,7 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType) return false; } - return ! $record->confidential() || hash_equals($record->secret, (string) $clientSecret); + return ! $record->confidential() || $this->verifySecret((string) $clientSecret, $record->secret); } /** @@ -84,4 +85,18 @@ protected function handlesGrant($record, $grantType) return true; } } + + /** + * @param string $clientSecret + * @param string $storedHash + * @return bool + */ + protected function verifySecret($clientSecret, $storedHash) + { + if (Passport::$useHashedClientSecrets) { + $clientSecret = hash('sha256', $clientSecret); + } + + return hash_equals($storedHash, $clientSecret); + } } diff --git a/src/Client.php b/src/Client.php index 51b2b7013..024bb1e1c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -41,6 +41,13 @@ class Client extends Model 'revoked' => 'bool', ]; + /** + * The temporary non-hashed client secret. + * + * @var string|null + */ + protected $plainSecret; + /** * Get the user that the client belongs to. * @@ -73,6 +80,36 @@ public function tokens() return $this->hasMany(Passport::tokenModel(), 'client_id'); } + /** + * The temporary non-hashed client secret. + * + * If you're using hashed client secrets, this value will only be available + * once during the request the client was created. Afterwards, it cannot + * be retrieved or decrypted anymore. + * + * @return string|null + */ + public function getPlainSecretAttribute() + { + return $this->plainSecret; + } + + /** + * @param string|null $value + */ + public function setSecretAttribute($value) + { + $this->plainSecret = $value; + + if ($value === null || ! Passport::$useHashedClientSecrets) { + $this->attributes['secret'] = $value; + + return; + } + + $this->attributes['secret'] = hash('sha256', $value); + } + /** * Determine if the client is a "first party" client. * diff --git a/src/Passport.php b/src/Passport.php index a68546951..e9b04fdb8 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -146,6 +146,11 @@ class Passport */ public static $unserializesCookies = false; + /** + * @var bool + */ + public static $useHashedClientSecrets = false; + /** * Indicates the scope should inherit its parent scope. * @@ -637,6 +642,18 @@ public static function ignoreMigrations() return new static; } + /** + * Configure Passport to hash client credential secrets. + * + * @return static + */ + public static function useHashedClientSecrets() + { + static::$useHashedClientSecrets = true; + + return new static; + } + /** * Instruct Passport to enable cookie serialization. * diff --git a/tests/BridgeClientRepositoryHashedSecretsTest.php b/tests/BridgeClientRepositoryHashedSecretsTest.php new file mode 100644 index 000000000..f834c79da --- /dev/null +++ b/tests/BridgeClientRepositoryHashedSecretsTest.php @@ -0,0 +1,29 @@ +shouldReceive('findActive') + ->with(1) + ->andReturn(new BridgeClientRepositoryHashedTestClientStub); + + $this->clientModelRepository = $clientModelRepository; + $this->repository = new BridgeClientRepository($clientModelRepository); + } +} + +class BridgeClientRepositoryHashedTestClientStub extends BridgeClientRepositoryTestClientStub +{ + public $secret = '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'; +} diff --git a/tests/BridgeClientRepositoryTest.php b/tests/BridgeClientRepositoryTest.php index 94a014323..1f0868de6 100644 --- a/tests/BridgeClientRepositoryTest.php +++ b/tests/BridgeClientRepositoryTest.php @@ -5,6 +5,7 @@ use Laravel\Passport\Bridge\Client; use Laravel\Passport\Bridge\ClientRepository as BridgeClientRepository; use Laravel\Passport\ClientRepository; +use Laravel\Passport\Passport; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -13,15 +14,17 @@ class BridgeClientRepositoryTest extends TestCase /** * @var \Laravel\Passport\ClientRepository */ - private $clientModelRepository; + protected $clientModelRepository; /** * @var \Laravel\Passport\Bridge\ClientRepository */ - private $repository; + protected $repository; protected function setUp(): void { + Passport::$useHashedClientSecrets = false; + $clientModelRepository = m::mock(ClientRepository::class); $clientModelRepository->shouldReceive('findActive') ->with(1)