Skip to content

Commit 2e44a19

Browse files
[13.x] Always hash client secret (#1745)
* always hash client secret * update upgrade guide * fix static analys * fix a bug on getting PAN client id * Update UPGRADE.md --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent fc7e2da commit 2e44a19

13 files changed

+50
-122
lines changed

UPGRADE.md

+10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ PR: https://github.com/laravel/passport/pull/1734
1616

1717
The `league/oauth2-server` Composer package which is utilized internally by Passport has been updated to 9.0, which adds additional types to method signatures. To ensure your application is compatible, you should review this package's complete [changelog](https://github.com/thephpleague/oauth2-server/blob/master/CHANGELOG.md#900---released-2024-05-13).
1818

19+
### Client Secrets Hashed by Default
20+
21+
PR: https://github.com/laravel/passport/pull/1745
22+
23+
Passport now always hashes client secrets using Laravel's `Hash` facade. If you are currently storing your client secrets in plain text, you may invoke the `passport:hash` Artisan command to hash all of your existing client secrets:
24+
25+
php artisan passport:hash
26+
27+
In light of this change, the `Passport::$hashesClientSecrets` property and `Passport::hashClientSecrets()` method has been removed.
28+
1929
## Upgrading To 12.0 From 11.x
2030

2131
### Migration Changes

src/Bridge/ClientRepository.php

+9-5
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace Laravel\Passport\Bridge;
44

5+
use Illuminate\Contracts\Hashing\Hasher;
56
use Laravel\Passport\Client as ClientModel;
67
use Laravel\Passport\ClientRepository as ClientModelRepository;
7-
use Laravel\Passport\Passport;
88
use League\OAuth2\Server\Entities\ClientEntityInterface;
99
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
1010

@@ -15,12 +15,18 @@ class ClientRepository implements ClientRepositoryInterface
1515
*/
1616
protected ClientModelRepository $clients;
1717

18+
/**
19+
* The hasher implementation.
20+
*/
21+
protected Hasher $hasher;
22+
1823
/**
1924
* Create a new repository instance.
2025
*/
21-
public function __construct(ClientModelRepository $clients)
26+
public function __construct(ClientModelRepository $clients, Hasher $hasher)
2227
{
2328
$this->clients = $clients;
29+
$this->hasher = $hasher;
2430
}
2531

2632
/**
@@ -83,8 +89,6 @@ protected function handlesGrant(ClientModel $record, string $grantType): bool
8389
*/
8490
protected function verifySecret(string $clientSecret, string $storedHash): bool
8591
{
86-
return Passport::$hashesClientSecrets
87-
? password_verify($clientSecret, $storedHash)
88-
: hash_equals($storedHash, $clientSecret);
92+
return $this->hasher->check($clientSecret, $storedHash);
8993
}
9094
}

src/Client.php

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Database\Eloquent\Factories\HasFactory;
66
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Support\Facades\Hash;
78
use Illuminate\Support\Str;
89
use Laravel\Passport\Database\Factories\ClientFactory;
910

@@ -129,13 +130,7 @@ public function setSecretAttribute($value)
129130
{
130131
$this->plainSecret = $value;
131132

132-
if (is_null($value) || ! Passport::$hashesClientSecrets) {
133-
$this->attributes['secret'] = $value;
134-
135-
return;
136-
}
137-
138-
$this->attributes['secret'] = password_hash($value, PASSWORD_BCRYPT);
133+
$this->attributes['secret'] = is_null($value) ? $value : Hash::make($value);
139134
}
140135

141136
/**

src/Console/ClientCommand.php

+3-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Illuminate\Console\Command;
66
use Laravel\Passport\Client;
77
use Laravel\Passport\ClientRepository;
8-
use Laravel\Passport\Passport;
98
use Symfony\Component\Console\Attribute\AsCommand;
109

1110
#[AsCommand(name: 'passport:client')]
@@ -164,12 +163,9 @@ protected function createAuthCodeClient(ClientRepository $clients)
164163
*/
165164
protected function outputClientDetails(Client $client)
166165
{
167-
if (Passport::$hashesClientSecrets) {
168-
$this->line('<comment>Here is your new client secret. This is the only time it will be shown so don\'t lose it!</comment>');
169-
$this->line('');
170-
}
166+
$this->components->warn('Here is your new client secret. This is the only time it will be shown so don\'t lose it!');
171167

172-
$this->components->twoColumnDetail('<comment>Client ID</comment>', $client->getKey());
173-
$this->components->twoColumnDetail('<comment>Client secret</comment>', $client->plainSecret);
168+
$this->components->twoColumnDetail('Client ID', $client->getKey());
169+
$this->components->twoColumnDetail('Client Secret', $client->plainSecret);
174170
}
175171
}

src/Console/HashCommand.php

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Laravel\Passport\Console;
44

55
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\Hash;
67
use Laravel\Passport\Passport;
78
use Symfony\Component\Console\Attribute\AsCommand;
89

@@ -30,17 +31,11 @@ class HashCommand extends Command
3031
*/
3132
public function handle()
3233
{
33-
if (! Passport::$hashesClientSecrets) {
34-
$this->components->warn('Please enable client hashing yet in your AppServiceProvider before continuing.');
35-
36-
return;
37-
}
38-
3934
if ($this->option('force') || $this->confirm('Are you sure you want to hash all client secrets? This cannot be undone.')) {
4035
$model = Passport::clientModel();
4136

4237
foreach ((new $model)->whereNotNull('secret')->cursor() as $client) {
43-
if (password_get_info($client->secret)['algo'] === PASSWORD_BCRYPT) {
38+
if (Hash::isHashed($client->secret) && ! Hash::needsRehash($client->secret)) {
4439
continue;
4540
}
4641

src/Http/Controllers/ClientController.php

+2-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Illuminate\Http\Response;
88
use Laravel\Passport\ClientRepository;
99
use Laravel\Passport\Http\Rules\RedirectRule;
10-
use Laravel\Passport\Passport;
1110

1211
class ClientController
1312
{
@@ -60,13 +59,7 @@ public function forUser(Request $request)
6059
{
6160
$userId = $request->user()->getAuthIdentifier();
6261

63-
$clients = $this->clients->activeForUser($userId);
64-
65-
if (Passport::$hashesClientSecrets) {
66-
return $clients;
67-
}
68-
69-
return $clients->makeVisible('secret');
62+
return $this->clients->activeForUser($userId);
7063
}
7164

7265
/**
@@ -88,9 +81,7 @@ public function store(Request $request)
8881
null, false, false, (bool) $request->input('confidential', true)
8982
);
9083

91-
if (Passport::$hashesClientSecrets) {
92-
return ['plainSecret' => $client->plainSecret] + $client->toArray();
93-
}
84+
$client->secret = $client->plainSecret;
9485

9586
return $client->makeVisible('secret');
9687
}

src/Passport.php

-19
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,6 @@ class Passport
156156
*/
157157
public static $decryptsCookies = true;
158158

159-
/**
160-
* Indicates if client secrets will be hashed.
161-
*
162-
* @var bool
163-
*/
164-
public static $hashesClientSecrets = false;
165-
166159
/**
167160
* The callback that should be used to generate JWT encryption keys.
168161
*
@@ -651,18 +644,6 @@ public static function refreshToken()
651644
return new static::$refreshTokenModel;
652645
}
653646

654-
/**
655-
* Configure Passport to hash client credential secrets.
656-
*
657-
* @return static
658-
*/
659-
public static function hashClientSecrets()
660-
{
661-
static::$hashesClientSecrets = true;
662-
663-
return new static;
664-
}
665-
666647
/**
667648
* Specify the callback that should be invoked to generate encryption keys for encrypting JWT tokens.
668649
*

src/PersonalAccessTokenFactory.php

+4-7
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function __construct(AuthorizationServer $server,
6969
public function make($userId, $name, array $scopes = [])
7070
{
7171
$response = $this->dispatchRequestToAuthorizationServer(
72-
$this->createRequest($this->clients->personalAccessClient(), $userId, $scopes)
72+
$this->createRequest($userId, $scopes)
7373
);
7474

7575
$token = tap($this->findAccessToken($response), function ($token) use ($userId, $name) {
@@ -87,19 +87,16 @@ public function make($userId, $name, array $scopes = [])
8787
/**
8888
* Create a request instance for the given client.
8989
*
90-
* @param \Laravel\Passport\Client $client
9190
* @param mixed $userId
9291
* @param array $scopes
9392
* @return \Psr\Http\Message\ServerRequestInterface
9493
*/
95-
protected function createRequest($client, $userId, array $scopes)
94+
protected function createRequest($userId, array $scopes)
9695
{
97-
$secret = Passport::$hashesClientSecrets ? $this->clients->getPersonalAccessClientSecret() : $client->secret;
98-
9996
return (new ServerRequest('POST', 'not-important'))->withParsedBody([
10097
'grant_type' => 'personal_access',
101-
'client_id' => $client->getKey(),
102-
'client_secret' => $secret,
98+
'client_id' => $this->clients->getPersonalAccessClientId(),
99+
'client_secret' => $this->clients->getPersonalAccessClientSecret(),
103100
'user_id' => $userId,
104101
'scope' => implode(' ', $scopes),
105102
]);

tests/Feature/AccessTokenControllerTest.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function testGettingAccessTokenWithClientCredentialsGrant()
3333
[
3434
'grant_type' => 'client_credentials',
3535
'client_id' => $client->getKey(),
36-
'client_secret' => $client->secret,
36+
'client_secret' => $client->plainSecret,
3737
]
3838
);
3939

@@ -76,7 +76,7 @@ public function testGettingAccessTokenWithClientCredentialsGrantInvalidClientSec
7676
[
7777
'grant_type' => 'client_credentials',
7878
'client_id' => $client->getKey(),
79-
'client_secret' => $client->secret.'foo',
79+
'client_secret' => $client->plainSecret.'foo',
8080
]
8181
);
8282

@@ -120,7 +120,7 @@ public function testGettingAccessTokenWithPasswordGrant()
120120
[
121121
'grant_type' => 'password',
122122
'client_id' => $client->getKey(),
123-
'client_secret' => $client->secret,
123+
'client_secret' => $client->plainSecret,
124124
'username' => $user->email,
125125
'password' => $password,
126126
]
@@ -169,7 +169,7 @@ public function testGettingAccessTokenWithPasswordGrantWithInvalidPassword()
169169
[
170170
'grant_type' => 'password',
171171
'client_id' => $client->getKey(),
172-
'client_secret' => $client->secret,
172+
'client_secret' => $client->plainSecret,
173173
'username' => $user->email,
174174
'password' => $password.'foo',
175175
]
@@ -213,7 +213,7 @@ public function testGettingAccessTokenWithPasswordGrantWithInvalidClientSecret()
213213
[
214214
'grant_type' => 'password',
215215
'client_id' => $client->getKey(),
216-
'client_secret' => $client->secret.'foo',
216+
'client_secret' => $client->plainSecret.'foo',
217217
'username' => $user->email,
218218
'password' => $password,
219219
]
@@ -258,7 +258,7 @@ public function testGettingCustomResponseType()
258258
[
259259
'grant_type' => 'client_credentials',
260260
'client_id' => $client->getKey(),
261-
'client_secret' => $client->secret,
261+
'client_secret' => $client->plainSecret,
262262
]
263263
);
264264

tests/Feature/Console/HashCommand.php

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@
44

55
use Illuminate\Contracts\Hashing\Hasher;
66
use Illuminate\Support\Str;
7-
use Laravel\Passport\Client;
8-
use Laravel\Passport\Passport;
7+
use Laravel\Passport\Database\Factories\ClientFactory;
98
use Laravel\Passport\Tests\Feature\PassportTestCase;
109

1110
class HashCommand extends PassportTestCase
1211
{
1312
public function test_it_can_properly_hash_client_secrets()
1413
{
15-
$client = factory(Client::class)->create(['secret' => $secret = Str::random(40)]);
14+
$client = ClientFactory::new()->create(['secret' => $secret = Str::random(40)]);
1615
$hasher = $this->app->make(Hasher::class);
1716

18-
Passport::hashClientSecrets();
19-
2017
$this->artisan('passport:hash', ['--force' => true]);
2118

2219
$this->assertTrue($hasher->check($secret, $client->refresh()->secret));
23-
24-
Passport::$hashesClientSecrets = false;
2520
}
2621
}

tests/Unit/BridgeClientRepositoryHashedSecretsTest.php

-29
This file was deleted.

tests/Unit/BridgeClientRepositoryTest.php

+8-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
namespace Laravel\Passport\Tests\Unit;
44

5+
use Illuminate\Contracts\Hashing\Hasher;
56
use Laravel\Passport\Bridge\Client;
67
use Laravel\Passport\Bridge\ClientRepository as BridgeClientRepository;
78
use Laravel\Passport\ClientRepository;
8-
use Laravel\Passport\Passport;
99
use Mockery as m;
1010
use PHPUnit\Framework\TestCase;
1111

@@ -23,15 +23,17 @@ class BridgeClientRepositoryTest extends TestCase
2323

2424
protected function setUp(): void
2525
{
26-
Passport::$hashesClientSecrets = false;
27-
2826
$clientModelRepository = m::mock(ClientRepository::class);
2927
$clientModelRepository->shouldReceive('findActive')
3028
->with(1)
31-
->andReturn(new BridgeClientRepositoryTestClientStub);
29+
->andReturn($client = new BridgeClientRepositoryTestClientStub);
30+
31+
$hasher = m::mock(Hasher::class);
32+
$hasher->shouldReceive('check')->with('secret', $client->secret)->andReturn(true);
33+
$hasher->shouldReceive('check')->withAnyArgs()->andReturn(false);
3234

3335
$this->clientModelRepository = $clientModelRepository;
34-
$this->repository = new BridgeClientRepository($clientModelRepository);
36+
$this->repository = new BridgeClientRepository($clientModelRepository, $hasher);
3537
}
3638

3739
protected function tearDown(): void
@@ -188,7 +190,7 @@ class BridgeClientRepositoryTestClientStub extends \Laravel\Passport\Client
188190

189191
public $redirect = 'http://localhost';
190192

191-
public $secret = 'secret';
193+
public $secret = '$2y$10$WgqU4wQpfsARCIQk.nPSOOiNkrMpPVxQiLCFUt8comvQwh1z6WFMG';
192194

193195
public $personal_access_client = false;
194196

0 commit comments

Comments
 (0)