Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[13.x] Improve issuing PATs #1780

Merged
merged 5 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ When authenticating users via bearer tokens, the `User` model's `token` method n

### Personal Access Client Table and Model Removal

PR: https://github.com/laravel/passport/pull/1749
PR: https://github.com/laravel/passport/pull/1749, https://github.com/laravel/passport/pull/1780

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:

Schema::drop('oauth_personal_access_clients');

In addition, the `Laravel\Passport\PersonalAccessClient` model, `Passport::$personalAccessClientModel` property, `Passport::usePersonalAccessClientModel()`, `Passport::personalAccessClientModel()`, and `Passport::personalAccessClient()` methods have been removed.
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.

## Upgrading To 12.0 From 11.x

Expand Down
16 changes: 0 additions & 16 deletions config/passport.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,4 @@

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

/*
|--------------------------------------------------------------------------
| Personal Access Client
|--------------------------------------------------------------------------
|
| If you enable client hashing, you should set the personal access client
| ID and unhashed secret within your environment file. The values will
| get used while issuing fresh personal access tokens to your users.
|
*/

'personal_access_client' => [
'id' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_ID'),
'secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'),
],

];
36 changes: 25 additions & 11 deletions src/Bridge/ClientRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,7 @@ public function getClientEntity(string $clientIdentifier): ?ClientEntityInterfac
{
$record = $this->clients->findActive($clientIdentifier);

if (! $record) {
return null;
}

return new Client(
$clientIdentifier,
$record->name,
$record->redirect_uris,
$record->confidential(),
$record->provider
);
return $record ? $this->fromClientModel($record) : null;
}

/**
Expand Down Expand Up @@ -81,4 +71,28 @@ protected function verifySecret(string $clientSecret, string $storedHash): bool
{
return $this->hasher->check($clientSecret, $storedHash);
}

/**
* Get the personal access client for the given provider.
*/
public function getPersonalAccessClientEntity(string $provider): ?ClientEntityInterface
{
return $this->fromClientModel(
$this->clients->personalAccessClient($provider)
);
}

/**
* Create a new client entity from the given client model instance.
*/
protected function fromClientModel(ClientModel $model): ClientEntityInterface
{
return new Client(
$model->getKey(),
$model->name,
$model->redirect_uris,
$model->confidential(),
$model->provider
);
}
}
18 changes: 7 additions & 11 deletions src/Bridge/PersonalAccessGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use DateInterval;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;

Expand All @@ -20,20 +19,17 @@ public function respondToAccessTokenRequest(
DateInterval $accessTokenTTL
): ResponseTypeInterface {
// Validate request
$client = $this->validateClient($request);

if (! $client->isConfidential()) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
if (! $userIdentifier = $this->getRequestParameter('user_id', $request)) {
throw OAuthServerException::invalidRequest('user_id');
}

throw OAuthServerException::invalidClient($request);
if (! $provider = $this->getRequestParameter('provider', $request)) {
throw OAuthServerException::invalidRequest('provider');
}

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

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

// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes(
Expand Down
24 changes: 24 additions & 0 deletions src/ClientRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Str;
use RuntimeException;

class ClientRepository
{
Expand Down Expand Up @@ -76,6 +77,29 @@ public function activeForUser($userId)
})->values();
}

/*
* Get the latest active personal access client for the given user provider.
*
* @throws \RuntimeException
*/
public function personalAccessClient(string $provider): Client
{
return Passport::client()
->where('revoked', false)
->whereNull('user_id')
->where(function ($query) use ($provider) {
$query->when($provider === config('auth.guards.api.provider'), function ($query) {
$query->orWhereNull('provider');
})->orWhere('provider', $provider);
})
->latest()
->get()
->first(fn (Client $client) => $client->hasGrantType('personal_access'))
?? throw new RuntimeException(
"Personal access client not found for '$provider' user provider. Please create one."
);
}

/**
* Store a new client.
*
Expand Down
12 changes: 3 additions & 9 deletions src/Console/ClientCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,13 @@ protected function createPersonalAccessClient(ClientRepository $clients)

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

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

$this->components->info('Personal access client created successfully.');

if (! config('passport.personal_access_client')) {
$this->components->info('Next, define the `PASSPORT_PERSONAL_ACCESS_CLIENT_ID` and `PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET` environment variables using the values below.');
}

$this->outputClientDetails($client);
}

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

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

Expand Down
24 changes: 1 addition & 23 deletions src/PersonalAccessTokenFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use Nyholm\Psr7\Response;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;

class PersonalAccessTokenFactory
{
Expand All @@ -18,13 +17,6 @@ class PersonalAccessTokenFactory
*/
protected $server;

/**
* The client repository instance.
*
* @var \Laravel\Passport\ClientRepository
*/
protected $clients;

/**
* The token repository instance.
*
Expand All @@ -43,20 +35,17 @@ class PersonalAccessTokenFactory
* Create a new personal access token factory instance.
*
* @param \League\OAuth2\Server\AuthorizationServer $server
* @param \Laravel\Passport\ClientRepository $clients
* @param \Laravel\Passport\TokenRepository $tokens
* @param \Lcobucci\JWT\Parser $jwt
* @return void
*/
public function __construct(AuthorizationServer $server,
ClientRepository $clients,
TokenRepository $tokens,
JwtParser $jwt)
{
$this->jwt = $jwt;
$this->tokens = $tokens;
$this->server = $server;
$this->clients = $clients;
}

/**
Expand Down Expand Up @@ -96,20 +85,9 @@ public function make($userId, string $name, array $scopes, string $provider)
*/
protected function createRequest($userId, array $scopes, string $provider)
{
$config = config("passport.personal_access_client.$provider", config('passport.personal_access_client'));

$client = isset($config['id']) ? $this->clients->findActive($config['id']) : null;

if (! $client || ($client->provider && $client->provider !== $provider)) {
throw new RuntimeException(
'Personal access client not found. Please create one and set the `PASSPORT_PERSONAL_ACCESS_CLIENT_ID` and `PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET` environment variables.'
);
}

return (new ServerRequest('POST', 'not-important'))->withParsedBody([
'grant_type' => 'personal_access',
'client_id' => $config['id'] ?? null,
'client_secret' => $config['secret'] ?? null,
'provider' => $provider,
'user_id' => $userId,
'scope' => implode(' ', $scopes),
]);
Expand Down
43 changes: 0 additions & 43 deletions tests/Feature/AccessTokenControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,49 +269,6 @@ public function testGettingCustomResponseType()
$this->assertArrayHasKey('id_token', $decodedResponse);
$this->assertSame('foo_bar_open_id_token', $decodedResponse['id_token']);
}

public function testPersonalAccessTokenRequestIsDisabled()
{
$user = UserFactory::new()->create([
'email' => '[email protected]',
'password' => $this->app->make(Hasher::class)->make('foobar123'),
]);

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

config([
'passport.personal_access_client.id' => $client->getKey(),
'passport.personal_access_client.secret' => $client->plainSecret,
]);

$response = $this->post(
'/oauth/token',
[
'grant_type' => 'personal_access',
'client_id' => $client->getKey(),
'client_secret' => $client->plainSecret,
'user_id' => $user->getKey(),
'scope' => '',
]
);

$response->assertStatus(400);

$decodedResponse = $response->decodeResponseJson()->json();

$this->assertArrayNotHasKey('token_type', $decodedResponse);
$this->assertArrayNotHasKey('expires_in', $decodedResponse);
$this->assertArrayNotHasKey('access_token', $decodedResponse);

$this->assertArrayHasKey('error', $decodedResponse);
$this->assertSame('unsupported_grant_type', $decodedResponse['error']);
$this->assertArrayHasKey('error_description', $decodedResponse);

$token = $user->createToken('test');

$this->assertInstanceOf(\Laravel\Passport\PersonalAccessTokenResult::class, $token);
}
}

class IdTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerTokenResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Laravel\Passport\Tests\Feature;

use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\DB;
use Laravel\Passport\Client;
Expand All @@ -13,25 +12,17 @@
use Orchestra\Testbench\Concerns\WithLaravelMigrations;
use Workbench\Database\Factories\UserFactory;

class PersonalAccessTokenFactoryTest extends PassportTestCase
class PersonalAccessGrantTest extends PassportTestCase
{
use WithLaravelMigrations;

public function testIssueToken()
{
$user = UserFactory::new()->create([
'email' => '[email protected]',
'password' => $this->app->make(Hasher::class)->make('foobar123'),
]);
$user = UserFactory::new()->create();

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

config([
'passport.personal_access_client.id' => $client->getKey(),
'passport.personal_access_client.secret' => $client->plainSecret,
]);

Passport::tokensCan([
'foo' => 'Do foo',
'bar' => 'Do bar',
Expand All @@ -56,9 +47,6 @@ public function testIssueTokenWithDifferentProviders()
'auth.guards.api-admins' => ['driver' => 'passport', 'provider' => 'admins'],
'auth.providers.customers' => ['driver' => 'eloquent', 'model' => CustomerProviderStub::class],
'auth.guards.api-customers' => ['driver' => 'passport', 'provider' => 'customers'],
'passport.personal_access_client' => ['id' => $client->getKey(), 'secret' => $client->plainSecret],
'passport.personal_access_client.admins' => ['id' => $adminClient->getKey(), 'secret' => $adminClient->plainSecret],
'passport.personal_access_client.customers' => ['id' => $customerClient->getKey(), 'secret' => $customerClient->plainSecret],
]);

$user = UserFactory::new()->create();
Expand Down Expand Up @@ -97,6 +85,28 @@ public function testIssueTokenWithDifferentProviders()
$this->assertEquals([$adminToken->token->id], $adminTokens);
$this->assertEquals([$customerToken->token->id], $customerTokens);
}

public function testPersonalAccessTokenRequestIsDisabled()
{
$user = UserFactory::new()->create();
$client = ClientFactory::new()->asPersonalAccessTokenClient()->create();

$response = $this->post('/oauth/token', [
'grant_type' => 'personal_access',
'provider' => $user->getProvider(),
'user_id' => $user->getKey(),
'scope' => '',
]);

$response->assertStatus(400);
$json = $response->json();

$this->assertSame('unsupported_grant_type', $json['error']);
$this->assertArrayHasKey('error_description', $json);
$this->assertArrayNotHasKey('access_token', $json);

$this->assertInstanceOf(PersonalAccessTokenResult::class, $user->createToken('test'));
}
}

class AdminProviderStub extends Authenticatable
Expand Down
1 change: 1 addition & 0 deletions tests/Unit/BridgeClientRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public function test_without_grant_types()
class BridgeClientRepositoryTestClientStub extends \Laravel\Passport\Client
{
protected $attributes = [
'id' => 1,
'name' => 'Client',
'redirect_uris' => '["http://localhost"]',
'secret' => '$2y$10$WgqU4wQpfsARCIQk.nPSOOiNkrMpPVxQiLCFUt8comvQwh1z6WFMG',
Expand Down