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] Determine if the client handles the specified grant #1762

Draft
wants to merge 9 commits into
base: 13.x
Choose a base branch
from
11 changes: 10 additions & 1 deletion src/Bridge/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,21 @@ public function __construct(
string $name,
array $redirectUri,
bool $isConfidential = false,
public ?string $provider = null
public ?string $provider = null,
public array $grantTypes = [],
) {
$this->setIdentifier($identifier);

$this->name = $name;
$this->isConfidential = $isConfidential;
$this->redirectUri = $redirectUri;
}

/**
* {@inheritdoc}
*/
public function hasGrantType(string $grantType): bool
{
return in_array($grantType, $this->grantTypes);
}
}
28 changes: 3 additions & 25 deletions src/Bridge/ClientRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,9 @@ public function getClientEntity(string $clientIdentifier): ?ClientEntityInterfac
*/
public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool
{
// First, we will verify that the client exists and is authorized to create personal
// access tokens. Generally personal access tokens are only generated by the user
// from the main interface. We'll only let certain clients generate the tokens.
$record = $this->clients->findActive($clientIdentifier);

if (! $record || ! $this->handlesGrant($record, $grantType)) {
return false;
}

return ! $record->confidential() || $this->verifySecret($clientSecret, $record->secret);
}

/**
* Determine if the given client can handle the given grant type.
*/
protected function handlesGrant(ClientModel $record, string $grantType): bool
{
return $record->hasGrantType($grantType);
}

/**
* Verify the client secret is valid.
*/
protected function verifySecret(string $clientSecret, string $storedHash): bool
{
return $this->hasher->check($clientSecret, $storedHash);
return $record && ! empty($clientSecret) && $this->hasher->check($clientSecret, $record->secret);
}

/**
Expand All @@ -82,7 +59,8 @@ protected function fromClientModel(ClientModel $model): ClientEntityInterface
$model->name,
$model->redirect_uris,
$model->confidential(),
$model->provider
$model->provider,
$model->grant_types
);
}
}
36 changes: 25 additions & 11 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ protected function redirectUris(): Attribute
);
}

/**
* Get the client's grant types.
*/
protected function grantTypes(): Attribute
{
return Attribute::make(
get: function (?string $value, array $attributes) {
if (isset($value)) {
return $this->fromJson($value);
}

return array_keys(array_filter([
'authorization_code' => ! empty($this->redirect_uris),
'client_credentials' => $this->confidential() && $this->firstParty(),
'implicit' => ! empty($this->redirect_uris),
'password' => $this->password_client,
'personal_access' => $this->personal_access_client,
'refresh_token' => true,
'urn:ietf:params:oauth:grant-type:device_code' => true,
]));
},
);
}

/**
* Determine if the client is a "first party" client.
*/
Expand All @@ -170,17 +194,7 @@ public function skipsAuthorization(Authenticatable $user, array $scopes): bool
*/
public function hasGrantType(string $grantType): bool
{
if (isset($this->attributes['grant_types']) && is_array($this->grant_types)) {
return in_array($grantType, $this->grant_types);
}

return match ($grantType) {
'authorization_code' => ! $this->personal_access_client && ! $this->password_client,
'personal_access' => $this->personal_access_client && $this->confidential(),
'password' => $this->password_client,
'client_credentials' => $this->confidential(),
default => true,
};
return in_array($grantType, $this->grant_types);
}

/**
Expand Down
63 changes: 63 additions & 0 deletions tests/Feature/AuthorizationCodeGrantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,67 @@ public function testPromptLogin()
$response->assertSessionHas('promptedForLogin', true);
$response->assertRedirectToRoute('login');
}

public function testUnauthorizedClient()
{
$client = ClientFactory::new()->create([
'grant_types' => [],
]);

$query = http_build_query([
'client_id' => $client->getKey(),
'redirect_uri' => $client->redirect_uris[0],
'response_type' => 'code',
]);

$user = UserFactory::new()->create();
$this->actingAs($user, 'web');

$json = $this->get('/oauth/authorize?'.$query)
->assertBadRequest()
->assertSessionMissing(['authRequest', 'authToken'])
->json();

$this->assertSame('unauthorized_client', $json['error']);
$this->assertSame(
'The authenticated client is not authorized to use this authorization grant type.',
$json['error_description']
);
}

public function testIssueAccessTokenWithoutRefreshToken()
{
$client = ClientFactory::new()->create([
'grant_types' => ['authorization_code'],
]);

$query = http_build_query([
'client_id' => $client->getKey(),
'redirect_uri' => $redirect = $client->redirect_uris[0],
'response_type' => 'code',
]);

$user = UserFactory::new()->create();
$this->actingAs($user, 'web');

$authToken = $this->get('/oauth/authorize?'.$query)
->assertOk()
->json('authToken');

$response = $this->post('/oauth/authorize', ['auth_token' => $authToken])->assertRedirect();
parse_str(parse_url($response->headers->get('Location'), PHP_URL_QUERY), $params);

$json = $this->post('/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => $client->getKey(),
'client_secret' => $client->plainSecret,
'redirect_uri' => $redirect,
'code' => $params['code'],
])->assertOk()->json();

$this->assertArrayHasKey('access_token', $json);
$this->assertArrayNotHasKey('refresh_token', $json);
$this->assertSame('Bearer', $json['token_type']);
$this->assertArrayHasKey('expires_in', $json);
}
}
17 changes: 17 additions & 0 deletions tests/Feature/ClientCredentialsGrantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,21 @@ public function testIssueAccessToken()
$response = $this->withToken($json['access_token'], $json['token_type'])->get('/bar');
$response->assertForbidden();
}

public function testUnauthorizedClient()
{
$client = ClientFactory::new()->create();

$json = $this->post('/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => $client->getKey(),
'client_secret' => $client->plainSecret,
])->assertBadRequest()->json();

$this->assertSame('unauthorized_client', $json['error']);
$this->assertSame(
'The authenticated client is not authorized to use this authorization grant type.',
$json['error_description']
);
}
}
22 changes: 18 additions & 4 deletions tests/Feature/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,19 @@ public function testGrantTypesWhenColumnDoesNotExist(): void
$client = new Client();
$client->exists = true;

$this->assertTrue($client->hasGrantType('foo'));

$client->personal_access_client = false;
$client->password_client = false;

$this->assertFalse($client->hasGrantType('foo'));
$this->assertFalse($client->hasGrantType('authorization_code'));
$this->assertFalse($client->hasGrantType('password'));
$this->assertFalse($client->hasGrantType('personal_access'));
$this->assertFalse($client->hasGrantType('client_credentials'));

$client->redirect = 'http://localhost';
$this->assertTrue($client->hasGrantType('authorization_code'));
$this->assertTrue($client->hasGrantType('implicit'));
unset($client->redirect);

$client->personal_access_client = false;
$client->password_client = true;
Expand All @@ -100,11 +107,18 @@ public function testGrantTypesWhenColumnIsNull(): void
$client = new Client(['grant_types' => null]);
$client->exists = true;

$this->assertTrue($client->hasGrantType('foo'));

$client->personal_access_client = false;
$client->password_client = false;
$this->assertFalse($client->hasGrantType('foo'));
$this->assertFalse($client->hasGrantType('authorization_code'));
$this->assertFalse($client->hasGrantType('password'));
$this->assertFalse($client->hasGrantType('personal_access'));
$this->assertFalse($client->hasGrantType('client_credentials'));

$client->redirect = 'http://localhost';
$this->assertTrue($client->hasGrantType('authorization_code'));
$this->assertTrue($client->hasGrantType('implicit'));
unset($client->redirect);

$client->personal_access_client = false;
$client->password_client = true;
Expand Down
25 changes: 25 additions & 0 deletions tests/Feature/ImplicitGrantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,29 @@ public function testPromptLogin()
$response->assertSessionHas('promptedForLogin', true);
$response->assertRedirectToRoute('login');
}

public function testUnauthorizedClient()
{
$client = ClientFactory::new()->create();

$query = http_build_query([
'client_id' => $client->getKey(),
'redirect_uri' => $client->redirect_uris[0],
'response_type' => 'token',
]);

$user = UserFactory::new()->create();
$this->actingAs($user, 'web');

$json = $this->get('/oauth/authorize?'.$query)
->assertBadRequest()
->assertSessionMissing(['authRequest', 'authToken'])
->json();

$this->assertSame('unauthorized_client', $json['error']);
$this->assertSame(
'The authenticated client is not authorized to use this authorization grant type.',
$json['error_description']
);
}
}
Loading
Loading