Skip to content

[13.x] Determine if the client handles the specified grant #1762

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

Merged
11 changes: 10 additions & 1 deletion src/Bridge/Client.php
Original file line number Diff line number Diff line change
@@ -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 supportsGrantType(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
@@ -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);
}

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

/**
* Interact with the client's grant types.
*/
protected function grantTypes(): Attribute
{
return Attribute::make(
get: fn (?string $value): array => isset($value) ? $this->fromJson($value) : 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 && $this->confidential(),
'refresh_token' => true,
'urn:ietf:params:oauth:grant-type:device_code' => true,
])),
);
}

/**
* Determine if the client is a "first party" client.
*/
@@ -170,17 +188,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);
}

/**
63 changes: 63 additions & 0 deletions tests/Feature/AuthorizationCodeGrantTest.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
31 changes: 31 additions & 0 deletions tests/Feature/ClientCredentialsGrantTest.php
Original file line number Diff line number Diff line change
@@ -54,4 +54,35 @@ public function testIssueAccessToken()
$response = $this->withToken($json['access_token'], $json['token_type'])->get('/bar');
$response->assertForbidden();
}

public function testPublicClient()
{
$client = ClientFactory::new()->asClientCredentials()->asPublic()->create();

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

$this->assertSame('invalid_client', $json['error']);
$this->assertSame('Client authentication failed', $json['error_description']);
}

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
@@ -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;
@@ -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;
25 changes: 25 additions & 0 deletions tests/Feature/ImplicitGrantTest.php
Original file line number Diff line number Diff line change
@@ -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