Skip to content

Commit 8375604

Browse files
[13.x] Improve issuing PATs (#1780)
* improve issuing pat * improve tests * upgrade guide * Update UPGRADE.md * Update ClientRepository.php --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 61644b3 commit 8375604

10 files changed

+87
-129
lines changed

UPGRADE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ When authenticating users via bearer tokens, the `User` model's `token` method n
5353

5454
### Personal Access Client Table and Model Removal
5555

56-
PR: https://github.com/laravel/passport/pull/1749
56+
PR: https://github.com/laravel/passport/pull/1749, https://github.com/laravel/passport/pull/1780
5757

5858
Passport's `oauth_personal_access_clients` table has been redundant and unnecessary for several release cycles. Therefore, this release of Passport no longer interacts with this table or its corresponding model. If you wish, you may create a migration that drops this table:
5959

6060
Schema::drop('oauth_personal_access_clients');
6161

62-
In addition, the `Laravel\Passport\PersonalAccessClient` model, `Passport::$personalAccessClientModel` property, `Passport::usePersonalAccessClientModel()`, `Passport::personalAccessClientModel()`, and `Passport::personalAccessClient()` methods have been removed.
62+
In addition, the `passport.personal_access_client` configuration value, `Laravel\Passport\PersonalAccessClient` model, `Passport::$personalAccessClientModel` property, `Passport::usePersonalAccessClientModel()`, `Passport::personalAccessClientModel()`, and `Passport::personalAccessClient()` methods have been removed.
6363

6464
## Upgrading To 12.0 From 11.x
6565

config/passport.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,4 @@
4343

4444
'connection' => env('PASSPORT_CONNECTION'),
4545

46-
/*
47-
|--------------------------------------------------------------------------
48-
| Personal Access Client
49-
|--------------------------------------------------------------------------
50-
|
51-
| If you enable client hashing, you should set the personal access client
52-
| ID and unhashed secret within your environment file. The values will
53-
| get used while issuing fresh personal access tokens to your users.
54-
|
55-
*/
56-
57-
'personal_access_client' => [
58-
'id' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_ID'),
59-
'secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'),
60-
],
61-
6246
];

src/Bridge/ClientRepository.php

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,7 @@ public function getClientEntity(string $clientIdentifier): ?ClientEntityInterfac
3636
{
3737
$record = $this->clients->findActive($clientIdentifier);
3838

39-
if (! $record) {
40-
return null;
41-
}
42-
43-
return new Client(
44-
$clientIdentifier,
45-
$record->name,
46-
$record->redirect_uris,
47-
$record->confidential(),
48-
$record->provider
49-
);
39+
return $record ? $this->fromClientModel($record) : null;
5040
}
5141

5242
/**
@@ -81,4 +71,28 @@ protected function verifySecret(string $clientSecret, string $storedHash): bool
8171
{
8272
return $this->hasher->check($clientSecret, $storedHash);
8373
}
74+
75+
/**
76+
* Get the personal access client for the given provider.
77+
*/
78+
public function getPersonalAccessClientEntity(string $provider): ?ClientEntityInterface
79+
{
80+
return $this->fromClientModel(
81+
$this->clients->personalAccessClient($provider)
82+
);
83+
}
84+
85+
/**
86+
* Create a new client entity from the given client model instance.
87+
*/
88+
protected function fromClientModel(ClientModel $model): ClientEntityInterface
89+
{
90+
return new Client(
91+
$model->getKey(),
92+
$model->name,
93+
$model->redirect_uris,
94+
$model->confidential(),
95+
$model->provider
96+
);
97+
}
8498
}

src/Bridge/PersonalAccessGrant.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use DateInterval;
66
use League\OAuth2\Server\Exception\OAuthServerException;
77
use League\OAuth2\Server\Grant\AbstractGrant;
8-
use League\OAuth2\Server\RequestEvent;
98
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
109
use Psr\Http\Message\ServerRequestInterface;
1110

@@ -20,20 +19,17 @@ public function respondToAccessTokenRequest(
2019
DateInterval $accessTokenTTL
2120
): ResponseTypeInterface {
2221
// Validate request
23-
$client = $this->validateClient($request);
24-
25-
if (! $client->isConfidential()) {
26-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
22+
if (! $userIdentifier = $this->getRequestParameter('user_id', $request)) {
23+
throw OAuthServerException::invalidRequest('user_id');
24+
}
2725

28-
throw OAuthServerException::invalidClient($request);
26+
if (! $provider = $this->getRequestParameter('provider', $request)) {
27+
throw OAuthServerException::invalidRequest('provider');
2928
}
3029

31-
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
32-
$userIdentifier = $this->getRequestParameter('user_id', $request);
30+
$client = $this->clientRepository->getPersonalAccessClientEntity($provider);
3331

34-
if (! $userIdentifier) {
35-
throw OAuthServerException::invalidRequest('user_id');
36-
}
32+
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
3733

3834
// Finalize the requested scopes
3935
$scopes = $this->scopeRepository->finalizeScopes(

src/ClientRepository.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Contracts\Auth\Authenticatable;
66
use Illuminate\Support\Str;
7+
use RuntimeException;
78

89
class ClientRepository
910
{
@@ -76,6 +77,29 @@ public function activeForUser($userId)
7677
})->values();
7778
}
7879

80+
/*
81+
* Get the latest active personal access client for the given user provider.
82+
*
83+
* @throws \RuntimeException
84+
*/
85+
public function personalAccessClient(string $provider): Client
86+
{
87+
return Passport::client()
88+
->where('revoked', false)
89+
->whereNull('user_id')
90+
->where(function ($query) use ($provider) {
91+
$query->when($provider === config('auth.guards.api.provider'), function ($query) {
92+
$query->orWhereNull('provider');
93+
})->orWhere('provider', $provider);
94+
})
95+
->latest()
96+
->get()
97+
->first(fn (Client $client) => $client->hasGrantType('personal_access'))
98+
?? throw new RuntimeException(
99+
"Personal access client not found for '$provider' user provider. Please create one."
100+
);
101+
}
102+
79103
/**
80104
* Store a new client.
81105
*

src/Console/ClientCommand.php

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,13 @@ protected function createPersonalAccessClient(ClientRepository $clients)
6868

6969
$provider = $this->option('provider') ?: $this->choice(
7070
'Which user provider should this client use to retrieve users?',
71-
array_keys(config('auth.providers')),
71+
collect(config('auth.guards'))->where('driver', 'passport')->pluck('provider')->all(),
7272
config('auth.guards.api.provider')
7373
);
7474

75-
$client = $clients->createPersonalAccessGrantClient($name, $provider);
75+
$clients->createPersonalAccessGrantClient($name, $provider);
7676

7777
$this->components->info('Personal access client created successfully.');
78-
79-
if (! config('passport.personal_access_client')) {
80-
$this->components->info('Next, define the `PASSPORT_PERSONAL_ACCESS_CLIENT_ID` and `PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET` environment variables using the values below.');
81-
}
82-
83-
$this->outputClientDetails($client);
8478
}
8579

8680
/**
@@ -98,7 +92,7 @@ protected function createPasswordClient(ClientRepository $clients)
9892

9993
$provider = $this->option('provider') ?: $this->choice(
10094
'Which user provider should this client use to retrieve users?',
101-
array_keys(config('auth.providers')),
95+
collect(config('auth.guards'))->where('driver', 'passport')->pluck('provider')->all(),
10296
config('auth.guards.api.provider')
10397
);
10498

src/PersonalAccessTokenFactory.php

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Nyholm\Psr7\Response;
88
use Nyholm\Psr7\ServerRequest;
99
use Psr\Http\Message\ServerRequestInterface;
10-
use RuntimeException;
1110

1211
class PersonalAccessTokenFactory
1312
{
@@ -18,13 +17,6 @@ class PersonalAccessTokenFactory
1817
*/
1918
protected $server;
2019

21-
/**
22-
* The client repository instance.
23-
*
24-
* @var \Laravel\Passport\ClientRepository
25-
*/
26-
protected $clients;
27-
2820
/**
2921
* The token repository instance.
3022
*
@@ -43,20 +35,17 @@ class PersonalAccessTokenFactory
4335
* Create a new personal access token factory instance.
4436
*
4537
* @param \League\OAuth2\Server\AuthorizationServer $server
46-
* @param \Laravel\Passport\ClientRepository $clients
4738
* @param \Laravel\Passport\TokenRepository $tokens
4839
* @param \Lcobucci\JWT\Parser $jwt
4940
* @return void
5041
*/
5142
public function __construct(AuthorizationServer $server,
52-
ClientRepository $clients,
5343
TokenRepository $tokens,
5444
JwtParser $jwt)
5545
{
5646
$this->jwt = $jwt;
5747
$this->tokens = $tokens;
5848
$this->server = $server;
59-
$this->clients = $clients;
6049
}
6150

6251
/**
@@ -96,20 +85,9 @@ public function make($userId, string $name, array $scopes, string $provider)
9685
*/
9786
protected function createRequest($userId, array $scopes, string $provider)
9887
{
99-
$config = config("passport.personal_access_client.$provider", config('passport.personal_access_client'));
100-
101-
$client = isset($config['id']) ? $this->clients->findActive($config['id']) : null;
102-
103-
if (! $client || ($client->provider && $client->provider !== $provider)) {
104-
throw new RuntimeException(
105-
'Personal access client not found. Please create one and set the `PASSPORT_PERSONAL_ACCESS_CLIENT_ID` and `PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET` environment variables.'
106-
);
107-
}
108-
10988
return (new ServerRequest('POST', 'not-important'))->withParsedBody([
11089
'grant_type' => 'personal_access',
111-
'client_id' => $config['id'] ?? null,
112-
'client_secret' => $config['secret'] ?? null,
90+
'provider' => $provider,
11391
'user_id' => $userId,
11492
'scope' => implode(' ', $scopes),
11593
]);

tests/Feature/AccessTokenControllerTest.php

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -269,49 +269,6 @@ public function testGettingCustomResponseType()
269269
$this->assertArrayHasKey('id_token', $decodedResponse);
270270
$this->assertSame('foo_bar_open_id_token', $decodedResponse['id_token']);
271271
}
272-
273-
public function testPersonalAccessTokenRequestIsDisabled()
274-
{
275-
$user = UserFactory::new()->create([
276-
'email' => '[email protected]',
277-
'password' => $this->app->make(Hasher::class)->make('foobar123'),
278-
]);
279-
280-
/** @var Client $client */
281-
$client = ClientFactory::new()->asPersonalAccessTokenClient()->create();
282-
283-
config([
284-
'passport.personal_access_client.id' => $client->getKey(),
285-
'passport.personal_access_client.secret' => $client->plainSecret,
286-
]);
287-
288-
$response = $this->post(
289-
'/oauth/token',
290-
[
291-
'grant_type' => 'personal_access',
292-
'client_id' => $client->getKey(),
293-
'client_secret' => $client->plainSecret,
294-
'user_id' => $user->getKey(),
295-
'scope' => '',
296-
]
297-
);
298-
299-
$response->assertStatus(400);
300-
301-
$decodedResponse = $response->decodeResponseJson()->json();
302-
303-
$this->assertArrayNotHasKey('token_type', $decodedResponse);
304-
$this->assertArrayNotHasKey('expires_in', $decodedResponse);
305-
$this->assertArrayNotHasKey('access_token', $decodedResponse);
306-
307-
$this->assertArrayHasKey('error', $decodedResponse);
308-
$this->assertSame('unsupported_grant_type', $decodedResponse['error']);
309-
$this->assertArrayHasKey('error_description', $decodedResponse);
310-
311-
$token = $user->createToken('test');
312-
313-
$this->assertInstanceOf(\Laravel\Passport\PersonalAccessTokenResult::class, $token);
314-
}
315272
}
316273

317274
class IdTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerTokenResponse

tests/Feature/PersonalAccessTokenFactoryTest.php renamed to tests/Feature/PersonalAccessGrantTest.php

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Laravel\Passport\Tests\Feature;
44

5-
use Illuminate\Contracts\Hashing\Hasher;
65
use Illuminate\Foundation\Auth\User as Authenticatable;
76
use Illuminate\Support\Facades\DB;
87
use Laravel\Passport\Client;
@@ -13,25 +12,17 @@
1312
use Orchestra\Testbench\Concerns\WithLaravelMigrations;
1413
use Workbench\Database\Factories\UserFactory;
1514

16-
class PersonalAccessTokenFactoryTest extends PassportTestCase
15+
class PersonalAccessGrantTest extends PassportTestCase
1716
{
1817
use WithLaravelMigrations;
1918

2019
public function testIssueToken()
2120
{
22-
$user = UserFactory::new()->create([
23-
'email' => '[email protected]',
24-
'password' => $this->app->make(Hasher::class)->make('foobar123'),
25-
]);
21+
$user = UserFactory::new()->create();
2622

2723
/** @var Client $client */
2824
$client = ClientFactory::new()->asPersonalAccessTokenClient()->create();
2925

30-
config([
31-
'passport.personal_access_client.id' => $client->getKey(),
32-
'passport.personal_access_client.secret' => $client->plainSecret,
33-
]);
34-
3526
Passport::tokensCan([
3627
'foo' => 'Do foo',
3728
'bar' => 'Do bar',
@@ -56,9 +47,6 @@ public function testIssueTokenWithDifferentProviders()
5647
'auth.guards.api-admins' => ['driver' => 'passport', 'provider' => 'admins'],
5748
'auth.providers.customers' => ['driver' => 'eloquent', 'model' => CustomerProviderStub::class],
5849
'auth.guards.api-customers' => ['driver' => 'passport', 'provider' => 'customers'],
59-
'passport.personal_access_client' => ['id' => $client->getKey(), 'secret' => $client->plainSecret],
60-
'passport.personal_access_client.admins' => ['id' => $adminClient->getKey(), 'secret' => $adminClient->plainSecret],
61-
'passport.personal_access_client.customers' => ['id' => $customerClient->getKey(), 'secret' => $customerClient->plainSecret],
6250
]);
6351

6452
$user = UserFactory::new()->create();
@@ -97,6 +85,28 @@ public function testIssueTokenWithDifferentProviders()
9785
$this->assertEquals([$adminToken->token->id], $adminTokens);
9886
$this->assertEquals([$customerToken->token->id], $customerTokens);
9987
}
88+
89+
public function testPersonalAccessTokenRequestIsDisabled()
90+
{
91+
$user = UserFactory::new()->create();
92+
$client = ClientFactory::new()->asPersonalAccessTokenClient()->create();
93+
94+
$response = $this->post('/oauth/token', [
95+
'grant_type' => 'personal_access',
96+
'provider' => $user->getProvider(),
97+
'user_id' => $user->getKey(),
98+
'scope' => '',
99+
]);
100+
101+
$response->assertStatus(400);
102+
$json = $response->json();
103+
104+
$this->assertSame('unsupported_grant_type', $json['error']);
105+
$this->assertArrayHasKey('error_description', $json);
106+
$this->assertArrayNotHasKey('access_token', $json);
107+
108+
$this->assertInstanceOf(PersonalAccessTokenResult::class, $user->createToken('test'));
109+
}
100110
}
101111

102112
class AdminProviderStub extends Authenticatable

tests/Unit/BridgeClientRepositoryTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ public function test_without_grant_types()
210210
class BridgeClientRepositoryTestClientStub extends \Laravel\Passport\Client
211211
{
212212
protected $attributes = [
213+
'id' => 1,
213214
'name' => 'Client',
214215
'redirect_uris' => '["http://localhost"]',
215216
'secret' => '$2y$10$WgqU4wQpfsARCIQk.nPSOOiNkrMpPVxQiLCFUt8comvQwh1z6WFMG',

0 commit comments

Comments
 (0)