diff --git a/Command/CreateClientCommand.php b/Command/CreateClientCommand.php index ac8dbba9..882a7a38 100644 --- a/Command/CreateClientCommand.php +++ b/Command/CreateClientCommand.php @@ -4,6 +4,7 @@ namespace Trikoder\Bundle\OAuth2Bundle\Command; +use InvalidArgumentException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -67,13 +68,27 @@ protected function configure(): void InputArgument::OPTIONAL, 'The client secret' ) + ->addOption( + 'public', + null, + InputOption::VALUE_NONE, + 'Create a public client.' + ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $client = $this->buildClientFromInput($input); + + try { + $client = $this->buildClientFromInput($input); + } catch (InvalidArgumentException $exception) { + $io->error($exception->getMessage()); + + return 1; + } + $this->clientManager->save($client); $io->success('New oAuth2 client created successfully.'); @@ -89,7 +104,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function buildClientFromInput(InputInterface $input): Client { $identifier = $input->getArgument('identifier') ?? hash('md5', random_bytes(16)); - $secret = $input->getArgument('secret') ?? hash('sha512', random_bytes(32)); + + $isPublic = $input->getOption('public'); + + if (null !== $input->getArgument('secret') && $isPublic) { + throw new InvalidArgumentException('The client cannot have a secret and be public.'); + } + + $secret = $isPublic ? null : $input->getArgument('secret') ?? hash('sha512', random_bytes(32)); $client = new Client($identifier, $secret); $client->setActive(true); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b991b1a0..ce4cb44b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -99,6 +99,10 @@ private function createAuthorizationServerNode(): NodeDefinition ->info('Whether to enable the authorization code grant') ->defaultTrue() ->end() + ->booleanNode('require_code_challenge_for_public_clients') + ->info('Whether to require code challenge for public clients for the auth code grant') + ->defaultTrue() + ->end() ->booleanNode('enable_implicit_grant') ->info('Whether to enable the implicit grant') ->defaultTrue() diff --git a/DependencyInjection/TrikoderOAuth2Extension.php b/DependencyInjection/TrikoderOAuth2Extension.php index df528be5..ed595dc7 100644 --- a/DependencyInjection/TrikoderOAuth2Extension.php +++ b/DependencyInjection/TrikoderOAuth2Extension.php @@ -199,15 +199,17 @@ private function configureGrants(ContainerBuilder $container, array $config): vo ]) ; - $container - ->getDefinition(AuthCodeGrant::class) - ->replaceArgument('$authCodeTTL', new Definition(DateInterval::class, [$config['auth_code_ttl']])) - ->addMethodCall('disableRequireCodeChallengeForPublicClients') // TODO: Make this grant option configurable + $authCodeGrantDefinition = $container->getDefinition(AuthCodeGrant::class); + $authCodeGrantDefinition->replaceArgument('$authCodeTTL', new Definition(DateInterval::class, [$config['auth_code_ttl']])) ->addMethodCall('setRefreshTokenTTL', [ new Definition(DateInterval::class, [$config['refresh_token_ttl']]), ]) ; + if (false === $config['require_code_challenge_for_public_clients']) { + $authCodeGrantDefinition->addMethodCall('disableRequireCodeChallengeForPublicClients'); + } + $container ->getDefinition(ImplicitGrant::class) ->replaceArgument('$accessTokenTTL', new Definition(DateInterval::class, [$config['access_token_ttl']])) diff --git a/League/Entity/Client.php b/League/Entity/Client.php index 3044b9c0..2f9e17f1 100644 --- a/League/Entity/Client.php +++ b/League/Entity/Client.php @@ -13,12 +13,6 @@ final class Client implements ClientEntityInterface use EntityTrait; use ClientTrait; - public function __construct() - { - // TODO: Add support for confidential clients - $this->isConfidential = true; - } - /** * {@inheritdoc} */ @@ -34,4 +28,9 @@ public function setRedirectUri(array $redirectUri): void { $this->redirectUri = $redirectUri; } + + public function setConfidential(bool $isConfidential): void + { + $this->isConfidential = $isConfidential; + } } diff --git a/League/Repository/ClientRepository.php b/League/Repository/ClientRepository.php index 936947fb..f876ed04 100644 --- a/League/Repository/ClientRepository.php +++ b/League/Repository/ClientRepository.php @@ -54,11 +54,7 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType) return false; } - if (null === $clientSecret) { - return true; - } - - if (hash_equals($client->getSecret(), (string) $clientSecret)) { + if (!$client->isConfidential() || hash_equals($client->getSecret(), (string) $clientSecret)) { return true; } @@ -70,6 +66,7 @@ private function buildClientEntity(ClientModel $client): ClientEntity $clientEntity = new ClientEntity(); $clientEntity->setIdentifier($client->getIdentifier()); $clientEntity->setRedirectUri(array_map('strval', $client->getRedirectUris())); + $clientEntity->setConfidential($client->isConfidential()); return $clientEntity; } diff --git a/Model/Client.php b/Model/Client.php index 9cc742f0..e21f6053 100644 --- a/Model/Client.php +++ b/Model/Client.php @@ -12,7 +12,7 @@ class Client private $identifier; /** - * @var string + * @var string|null */ private $secret; @@ -36,7 +36,7 @@ class Client */ private $active = true; - public function __construct(string $identifier, string $secret) + public function __construct(string $identifier, ?string $secret) { $this->identifier = $identifier; $this->secret = $secret; @@ -52,7 +52,7 @@ public function getIdentifier(): string return $this->identifier; } - public function getSecret(): string + public function getSecret(): ?string { return $this->secret; } @@ -113,4 +113,9 @@ public function setActive(bool $active): self return $this; } + + public function isConfidential(): bool + { + return !empty($this->secret); + } } diff --git a/README.md b/README.md index d195db0e..84a57847 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ This package is currently in the active development. # Whether to enable the authorization code grant enable_auth_code_grant: true + # Whether to require code challenge for public clients for the auth code grant + require_code_challenge_for_public_clients: true + # Whether to enable the implicit grant enable_implicit_grant: true resource_server: # Required diff --git a/Resources/config/doctrine/model/Client.orm.xml b/Resources/config/doctrine/model/Client.orm.xml index 804dffca..ae7371c4 100644 --- a/Resources/config/doctrine/model/Client.orm.xml +++ b/Resources/config/doctrine/model/Client.orm.xml @@ -4,7 +4,7 @@ https://raw.github.com/doctrine/doctrine2/master/doctrine-mapping.xsd"> - + diff --git a/Tests/Acceptance/AuthorizationEndpointTest.php b/Tests/Acceptance/AuthorizationEndpointTest.php index faab665a..e706a43a 100644 --- a/Tests/Acceptance/AuthorizationEndpointTest.php +++ b/Tests/Acceptance/AuthorizationEndpointTest.php @@ -12,8 +12,10 @@ use Trikoder\Bundle\OAuth2Bundle\Manager\ClientManagerInterface; use Trikoder\Bundle\OAuth2Bundle\Manager\RefreshTokenManagerInterface; use Trikoder\Bundle\OAuth2Bundle\Manager\ScopeManagerInterface; +use Trikoder\Bundle\OAuth2Bundle\Model\AuthorizationCode; use Trikoder\Bundle\OAuth2Bundle\OAuth2Events; use Trikoder\Bundle\OAuth2Bundle\Tests\Fixtures\FixtureFactory; +use Trikoder\Bundle\OAuth2Bundle\Tests\TestHelper; final class AuthorizationEndpointTest extends AbstractAcceptanceTest { @@ -68,6 +70,117 @@ public function testSuccessfulCodeRequest(): void $this->assertEquals('foobar', $query['state']); } + public function testSuccessfulPKCEAuthCodeRequest(): void + { + $state = bin2hex(random_bytes(20)); + $codeVerifier = bin2hex(random_bytes(64)); + $codeChallengeMethod = 'S256'; + + $codeChallenge = strtr( + rtrim(base64_encode(hash('sha256', $codeVerifier, true)), '='), + '+/', + '-_' + ); + + $this->client + ->getContainer() + ->get('event_dispatcher') + ->addListener(OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE, function (AuthorizationRequestResolveEvent $event) use ($state, $codeChallenge, $codeChallengeMethod): void { + $this->assertSame($state, $event->getState()); + $this->assertSame($codeChallenge, $event->getCodeChallenge()); + $this->assertSame($codeChallengeMethod, $event->getCodeChallengeMethod()); + + $event->resolveAuthorization(AuthorizationRequestResolveEvent::AUTHORIZATION_APPROVED); + }); + + timecop_freeze(new DateTimeImmutable()); + + try { + $this->client->request( + 'GET', + '/authorize', + [ + 'client_id' => FixtureFactory::FIXTURE_PUBLIC_CLIENT, + 'response_type' => 'code', + 'scope' => '', + 'state' => $state, + 'code_challenge' => $codeChallenge, + 'code_challenge_method' => $codeChallengeMethod, + ] + ); + } finally { + timecop_return(); + } + + $response = $this->client->getResponse(); + + $this->assertSame(302, $response->getStatusCode()); + $redirectUri = $response->headers->get('Location'); + + $this->assertStringStartsWith(FixtureFactory::FIXTURE_CLIENT_FIRST_REDIRECT_URI, $redirectUri); + $query = []; + parse_str(parse_url($redirectUri, PHP_URL_QUERY), $query); + $this->assertArrayHasKey('state', $query); + $this->assertSame($state, $query['state']); + + $this->assertArrayHasKey('code', $query); + $payload = json_decode(TestHelper::decryptPayload($query['code']), true); + + $this->assertArrayHasKey('code_challenge', $payload); + $this->assertArrayHasKey('code_challenge_method', $payload); + $this->assertSame($codeChallenge, $payload['code_challenge']); + $this->assertSame($codeChallengeMethod, $payload['code_challenge_method']); + + /** @var AuthorizationCode|null $authCode */ + $authCode = $this->client + ->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository(AuthorizationCode::class) + ->findOneBy(['identifier' => $payload['auth_code_id']]); + + $this->assertInstanceOf(AuthorizationCode::class, $authCode); + $this->assertSame(FixtureFactory::FIXTURE_PUBLIC_CLIENT, $authCode->getClient()->getIdentifier()); + } + + public function testAuthCodeRequestWithPublicClientWithoutCodeChallengeWhenTheChallengeIsRequiredForPublicClients(): void + { + $this->client + ->getContainer() + ->get('event_dispatcher') + ->addListener(OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE, function (AuthorizationRequestResolveEvent $event): void { + $this->fail('This event should not have been dispatched.'); + }); + + timecop_freeze(new DateTimeImmutable()); + + try { + $this->client->request( + 'GET', + '/authorize', + [ + 'client_id' => FixtureFactory::FIXTURE_PUBLIC_CLIENT, + 'response_type' => 'code', + 'scope' => '', + 'state' => bin2hex(random_bytes(20)), + ] + ); + } finally { + timecop_return(); + } + + $response = $this->client->getResponse(); + + $this->assertSame(400, $response->getStatusCode()); + + $this->assertSame('application/json', $response->headers->get('Content-Type')); + + $jsonResponse = json_decode($response->getContent(), true); + + $this->assertSame('invalid_request', $jsonResponse['error']); + $this->assertSame('The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.', $jsonResponse['message']); + $this->assertSame('Code challenge must be provided for public clients', $jsonResponse['hint']); + } + public function testSuccessfulTokenRequest(): void { $this->client diff --git a/Tests/Acceptance/CreateClientCommandTest.php b/Tests/Acceptance/CreateClientCommandTest.php index de3a9da4..0c9a8fda 100644 --- a/Tests/Acceptance/CreateClientCommandTest.php +++ b/Tests/Acceptance/CreateClientCommandTest.php @@ -35,11 +35,66 @@ public function testCreateClientWithIdentifier(): void $this->assertStringContainsString('New oAuth2 client created successfully', $output); $this->assertStringContainsString('foobar', $output); + /** @var Client $client */ $client = $this->client ->getContainer() ->get(ClientManagerInterface::class) ->find('foobar'); $this->assertInstanceOf(Client::class, $client); + $this->assertTrue($client->isConfidential()); + $this->assertNotEmpty($client->getSecret()); + } + + public function testCreatePublicClientWithIdentifier(): void + { + $clientIdentifier = 'foobar test'; + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $clientIdentifier, + '--public' => true, + ]); + + $this->assertSame(0, $commandTester->getStatusCode()); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('New oAuth2 client created successfully', $output); + $this->assertStringContainsString($clientIdentifier, $output); + + /** @var Client $client */ + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find($clientIdentifier); + $this->assertInstanceOf(Client::class, $client); + $this->assertFalse($client->isConfidential()); + $this->assertNull($client->getSecret()); + } + + public function testCannotCreatePublicClientWithSecret(): void + { + $clientIdentifier = 'foobar test'; + $command = $this->application->find('trikoder:oauth2:create-client'); + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'identifier' => $clientIdentifier, + 'secret' => 'foo', + '--public' => true, + ]); + + $this->assertSame(1, $commandTester->getStatusCode()); + + $output = $commandTester->getDisplay(); + $this->assertStringContainsString('The client cannot have a secret and be public.', $output); + $this->assertStringNotContainsString($clientIdentifier, $output); + + $client = $this->client + ->getContainer() + ->get(ClientManagerInterface::class) + ->find($clientIdentifier); + $this->assertNull($client); } public function testCreateClientWithSecret(): void @@ -54,12 +109,15 @@ public function testCreateClientWithSecret(): void $output = $commandTester->getDisplay(); $this->assertStringContainsString('New oAuth2 client created successfully', $output); + + /** @var Client $client */ $client = $this->client ->getContainer() ->get(ClientManagerInterface::class) ->find('foobar'); $this->assertInstanceOf(Client::class, $client); $this->assertSame('quzbaz', $client->getSecret()); + $this->assertTrue($client->isConfidential()); } public function testCreateClientWithRedirectUris(): void diff --git a/Tests/Acceptance/TokenEndpointTest.php b/Tests/Acceptance/TokenEndpointTest.php index f219fa41..08124e61 100644 --- a/Tests/Acceptance/TokenEndpointTest.php +++ b/Tests/Acceptance/TokenEndpointTest.php @@ -157,6 +157,38 @@ public function testSuccessfulAuthorizationCodeRequest(): void $this->assertNotEmpty($jsonResponse['access_token']); } + public function testSuccessfulAuthorizationCodeRequestWithPublicClient(): void + { + $authCode = $this->client + ->getContainer() + ->get(AuthorizationCodeManagerInterface::class) + ->find(FixtureFactory::FIXTURE_AUTH_CODE_PUBLIC_CLIENT); + + timecop_freeze(new DateTimeImmutable()); + + try { + $this->client->request('POST', '/token', [ + 'client_id' => FixtureFactory::FIXTURE_PUBLIC_CLIENT, + 'grant_type' => 'authorization_code', + 'redirect_uri' => FixtureFactory::FIXTURE_PUBLIC_CLIENT_REDIRECT_URI, + 'code' => TestHelper::generateEncryptedAuthCodePayload($authCode), + ]); + } finally { + timecop_return(); + } + + $response = $this->client->getResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json; charset=UTF-8', $response->headers->get('Content-Type')); + + $jsonResponse = json_decode($response->getContent(), true); + + $this->assertSame('Bearer', $jsonResponse['token_type']); + $this->assertSame(3600, $jsonResponse['expires_in']); + $this->assertNotEmpty($jsonResponse['access_token']); + } + public function testFailedTokenRequest(): void { $this->client->request('POST', '/token'); diff --git a/Tests/Fixtures/FixtureFactory.php b/Tests/Fixtures/FixtureFactory.php index e4edd00d..a9bd3484 100644 --- a/Tests/Fixtures/FixtureFactory.php +++ b/Tests/Fixtures/FixtureFactory.php @@ -41,6 +41,7 @@ final class FixtureFactory public const FIXTURE_REFRESH_TOKEN_WITH_SCOPES = 'e47d593ed661840b3633e4577c3261ef57ba225be193b190deb69ee9afefdc19f54f890fbdda59f5'; public const FIXTURE_AUTH_CODE = '0aa70e8152259988b3c8e9e8cff604019bb986eb226bd126da189829b95a2be631e2506042064e12'; + public const FIXTURE_AUTH_CODE_PUBLIC_CLIENT = 'xaa70e8152259988b3c8e9e8cff604019bb986eb226bd126da189829b95a2be631e2506042064e12'; public const FIXTURE_AUTH_CODE_DIFFERENT_CLIENT = 'e8fe264053cb346f4437af05c8cc9036931cfec3a0d5b54bdae349304ca4a83fd2f4590afd51e559'; public const FIXTURE_AUTH_CODE_EXPIRED = 'a7bdbeb26c9f095d842f5e5b8e313b24318d6b26728d1c543136727bbe9525f7a7930305a09b7401'; @@ -49,8 +50,10 @@ final class FixtureFactory public const FIXTURE_CLIENT_INACTIVE = 'baz_inactive'; public const FIXTURE_CLIENT_RESTRICTED_GRANTS = 'qux_restricted_grants'; public const FIXTURE_CLIENT_RESTRICTED_SCOPES = 'quux_restricted_scopes'; + public const FIXTURE_PUBLIC_CLIENT = 'foo_public'; public const FIXTURE_CLIENT_FIRST_REDIRECT_URI = 'https://example.org/oauth2/redirect-uri'; + public const FIXTURE_PUBLIC_CLIENT_REDIRECT_URI = 'https://example.org/oauth2/redirect-uri-foo-test'; public const FIXTURE_SCOPE_FIRST = 'fancy'; public const FIXTURE_SCOPE_SECOND = 'rock'; @@ -217,6 +220,14 @@ public static function createAuthorizationCodes(ClientManagerInterface $clientMa [] ); + $authorizationCodes[] = new AuthorizationCode( + self::FIXTURE_AUTH_CODE_PUBLIC_CLIENT, + new DateTimeImmutable('+2 minute'), + $clientManager->find(self::FIXTURE_PUBLIC_CLIENT), + self::FIXTURE_USER, + [] + ); + $authorizationCodes[] = new AuthorizationCode( self::FIXTURE_AUTH_CODE_DIFFERENT_CLIENT, new DateTimeImmutable('+2 minute'), @@ -257,6 +268,9 @@ private static function createClients(): array $clients[] = (new Client(self::FIXTURE_CLIENT_RESTRICTED_SCOPES, 'beer')) ->setScopes(new Scope(self::FIXTURE_SCOPE_SECOND)); + $clients[] = (new Client(self::FIXTURE_PUBLIC_CLIENT, null)) + ->setRedirectUris(new RedirectUri(self::FIXTURE_PUBLIC_CLIENT_REDIRECT_URI)); + return $clients; } diff --git a/Tests/Integration/AbstractIntegrationTest.php b/Tests/Integration/AbstractIntegrationTest.php index ae917c40..9bcb6684 100644 --- a/Tests/Integration/AbstractIntegrationTest.php +++ b/Tests/Integration/AbstractIntegrationTest.php @@ -98,6 +98,11 @@ abstract class AbstractIntegrationTest extends TestCase */ private $psrFactory; + /** + * @var bool + */ + private $requireCodeChallengeForPublicClients = true; + /** * {@inheritdoc} */ @@ -251,6 +256,16 @@ protected function extractQueryDataFromUri(string $uri): array return $data; } + protected function enableRequireCodeChallengeForPublicClients(): void + { + $this->requireCodeChallengeForPublicClients = true; + } + + protected function disableRequireCodeChallengeForPublicClients(): void + { + $this->requireCodeChallengeForPublicClients = false; + } + private function createAuthorizationServer( ScopeRepositoryInterface $scopeRepository, ClientRepositoryInterface $clientRepository, @@ -267,9 +282,11 @@ private function createAuthorizationServer( TestHelper::ENCRYPTION_KEY ); - // TODO: Make this grant option configurable $authCodeGrant = new AuthCodeGrant($authCodeRepository, $refreshTokenRepository, new DateInterval('PT10M')); - $authCodeGrant->disableRequireCodeChallengeForPublicClients(); + + if (!$this->requireCodeChallengeForPublicClients) { + $authCodeGrant->disableRequireCodeChallengeForPublicClients(); + } $authorizationServer->enableGrantType(new ClientCredentialsGrant()); $authorizationServer->enableGrantType(new RefreshTokenGrant($refreshTokenRepository)); diff --git a/Tests/TestHelper.php b/Tests/TestHelper.php index 427c9f82..22ee5f64 100644 --- a/Tests/TestHelper.php +++ b/Tests/TestHelper.php @@ -63,6 +63,15 @@ public static function generateEncryptedAuthCodePayload(AuthorizationCodeModel $ } } + public static function decryptPayload(string $payload): ?string + { + try { + return Crypto::decryptWithPassword($payload, self::ENCRYPTION_KEY); + } catch (CryptoException $e) { + return null; + } + } + public static function generateJwtToken(AccessTokenModel $accessToken): string { $clientEntity = new ClientEntity(); diff --git a/Tests/Unit/ClientEntityTest.php b/Tests/Unit/ClientEntityTest.php new file mode 100644 index 00000000..aced381c --- /dev/null +++ b/Tests/Unit/ClientEntityTest.php @@ -0,0 +1,30 @@ +assertSame($isConfidential, $client->isConfidential()); + } + + public function confidentialDataProvider(): iterable + { + return [ + 'Client with null secret is not confidential' => [null, false], + 'Client with empty secret is not confidential' => ['', false], + 'Client with non empty secret is confidential' => ['f', true], + ]; + } +} diff --git a/Tests/Unit/ExtensionTest.php b/Tests/Unit/ExtensionTest.php index fd09145e..65081994 100644 --- a/Tests/Unit/ExtensionTest.php +++ b/Tests/Unit/ExtensionTest.php @@ -5,6 +5,7 @@ namespace Trikoder\Bundle\OAuth2Bundle\Tests\Unit; use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Grant\AuthCodeGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\RefreshTokenGrant; @@ -64,7 +65,53 @@ public function grantsProvider(): iterable ]; } - private function getValidConfiguration(array $options): array + /** + * @dataProvider requireCodeChallengeForPublicClientsProvider + */ + public function testAuthCodeGrantDisableRequireCodeChallengeForPublicClientsConfig( + ?bool $requireCodeChallengeForPublicClients, + bool $shouldTheRequirementBeDisabled + ): void { + $container = new ContainerBuilder(); + + $this->setupContainer($container); + + $extension = new TrikoderOAuth2Extension(); + + $configuration = $this->getValidConfiguration(); + $configuration[0]['authorization_server']['require_code_challenge_for_public_clients'] = $requireCodeChallengeForPublicClients; + + $extension->load($configuration, $container); + + $authorizationServer = $container->getDefinition(AuthCodeGrant::class); + $methodCalls = $authorizationServer->getMethodCalls(); + + $isRequireCodeChallengeForPublicClientsDisabled = false; + + foreach ($methodCalls as $methodCall) { + if ('disableRequireCodeChallengeForPublicClients' === $methodCall[0]) { + $isRequireCodeChallengeForPublicClientsDisabled = true; + break; + } + } + + $this->assertSame($shouldTheRequirementBeDisabled, $isRequireCodeChallengeForPublicClientsDisabled); + } + + public function requireCodeChallengeForPublicClientsProvider(): iterable + { + yield 'when not requiring code challenge for public clients the requirement should be disabled' => [ + false, true, + ]; + yield 'when code challenge for public clients is required the requirement should not be disabled' => [ + true, false, + ]; + yield 'with the default value the requirement should not be disabled' => [ + null, false, + ]; + } + + private function getValidConfiguration(array $options = []): array { return [ [ diff --git a/docs/basic-setup.md b/docs/basic-setup.md index b5642846..636ca159 100644 --- a/docs/basic-setup.md +++ b/docs/basic-setup.md @@ -23,6 +23,7 @@ Options: --redirect-uri[=REDIRECT-URI] Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs. (multiple values allowed) --grant-type[=GRANT-TYPE] Sets allowed grant type for client. Use this option multiple times to set multiple grant types. (multiple values allowed) --scope[=SCOPE] Sets allowed scope for client. Use this option multiple times to set multiple scopes. (multiple values allowed) + --public Creates a public client (a client which does not have a secret) ```