From a793df2b31ed36a1f90492b105009ceeec72d964 Mon Sep 17 00:00:00 2001 From: Kentaro Ohkouchi Date: Mon, 24 Jul 2023 11:14:54 +0900 Subject: [PATCH 1/5] Support grant_type=password --- Controller/Admin/OAuthController.php | 16 +++++----------- Resource/config/services.yaml | 5 +++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Controller/Admin/OAuthController.php b/Controller/Admin/OAuthController.php index 9bdd25b..76c268b 100644 --- a/Controller/Admin/OAuthController.php +++ b/Controller/Admin/OAuthController.php @@ -108,7 +108,7 @@ public function create(Request $request) $secret = $form->get('secret')->getData(); try { - $client = new Client($name, $identifier, $secret); + $client = new Client($name, $identifier, null); // FIXME Set client_secret to null $client = $this->updateClientFromForm($client, $form); $this->clientManager->save($client); @@ -209,16 +209,10 @@ function (string $redirectUri): RedirectUri { ); $client->setRedirectUris(...$redirectUris); - $grants = array_map( - function (string $grant): Grant { - return new Grant($grant); - }, - $form->get('grants')->getData() - ); - // authorization code grant が選択されていた場合には refresh token grant も付与 - if (in_array(OAuth2Grants::AUTHORIZATION_CODE, $grants)) { - array_push($grants, new Grant(OAuth2Grants::REFRESH_TOKEN)); - } + $grants = [ + new Grant(OAuth2Grants::PASSWORD), + new Grant(OAuth2Grants::REFRESH_TOKEN) + ]; $client->setGrants(...$grants); $scopes = array_map( diff --git a/Resource/config/services.yaml b/Resource/config/services.yaml index a562b9b..f365c00 100644 --- a/Resource/config/services.yaml +++ b/Resource/config/services.yaml @@ -17,7 +17,7 @@ league_oauth2_server: enable_client_credentials_grant: false # Whether to enable the password grant - enable_password_grant: false + enable_password_grant: true # Whether to enable the refresh token grant enable_refresh_token_grant: true @@ -41,7 +41,8 @@ league_oauth2_server: services: Plugin\Api42\EventListener\UserResolveListener: arguments: - - '@Eccube\Security\Core\User\MemberProvider' + # - '@Eccube\Security\Core\User\MemberProvider' + - '@Eccube\Security\Core\User\CustomerProvider' # FIXME - '@Eccube\Security\Core\User\UserPasswordHasher' tags: - { name: kernel.event_listener, event: league.oauth2_server.event.user_resolve, method: onUserResolve } From 4510ae574bd659352ec745ba4da6bca16141a201 Mon Sep 17 00:00:00 2001 From: Kentaro Ohkouchi Date: Thu, 27 Jul 2023 14:51:44 +0900 Subject: [PATCH 2/5] Support CustomerProvider and MemberProvider --- EventListener/UserResolveListener.php | 30 +++++++++++++++++++-------- Resource/config/services.yaml | 8 ++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/EventListener/UserResolveListener.php b/EventListener/UserResolveListener.php index e5dbf8e..ca42cf4 100644 --- a/EventListener/UserResolveListener.php +++ b/EventListener/UserResolveListener.php @@ -15,15 +15,21 @@ use Eccube\Security\Core\User\UserPasswordHasher; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\UserProviderInterface; use League\Bundle\OAuth2ServerBundle\Event\UserResolveEvent; final class UserResolveListener { /** - * @var UserProviderInterface + * @var CustomerProvider */ - private $userProvider; + private $customerProvider; + + /** + * @var MemberProvider + */ + private $memberProvider; /** * @var UserPasswordHasher @@ -31,12 +37,14 @@ final class UserResolveListener private $userPasswordEncoder; /** - * @param UserProviderInterface $userProvider + * @param UserProviderInterface $customerProvider + * @param UserProviderInterface $memberProvider * @param UserPasswordHasher $userPasswordEncoder */ - public function __construct(UserProviderInterface $userProvider, UserPasswordHasher $userPasswordEncoder) + public function __construct(UserProviderInterface $customerProvider, UserProviderInterface $memberProvider, UserPasswordHasher $userPasswordEncoder) { - $this->userProvider = $userProvider; + $this->customerProvider = $customerProvider; + $this->memberProvider = $memberProvider; $this->userPasswordEncoder = $userPasswordEncoder; } @@ -45,10 +53,14 @@ public function __construct(UserProviderInterface $userProvider, UserPasswordHas */ public function onUserResolve(UserResolveEvent $event): void { - $user = $this->userProvider->loadUserByUsername($event->getUsername()); - - if (null === $user) { - return; + try { + $user = $this->customerProvider->loadUserByUsername($event->getUsername()); + } catch (UserNotFoundException $e) { + try { + $user = $this->memberProvider->loadUserByUsername($event->getUsername()); + } catch (UserNotFoundException $e) { + return; + } } if (!$this->userPasswordEncoder->isPasswordValid($user, $event->getPassword())) { diff --git a/Resource/config/services.yaml b/Resource/config/services.yaml index f365c00..8e2b242 100644 --- a/Resource/config/services.yaml +++ b/Resource/config/services.yaml @@ -39,10 +39,12 @@ league_oauth2_server: doctrine: null services: - Plugin\Api42\EventListener\UserResolveListener: + Symfony\Component\Security\Core\User\UserProviderInterface: '@security.user_providers' + api.event.user_resolve: + class: Plugin\Api42\EventListener\UserResolveListener arguments: - # - '@Eccube\Security\Core\User\MemberProvider' - - '@Eccube\Security\Core\User\CustomerProvider' # FIXME + - '@Eccube\Security\Core\User\CustomerProvider' + - '@Eccube\Security\Core\User\MemberProvider' - '@Eccube\Security\Core\User\UserPasswordHasher' tags: - { name: kernel.event_listener, event: league.oauth2_server.event.user_resolve, method: onUserResolve } From 2912bd06080e6bccf95bdc9cc16d3cc21bebb1fa Mon Sep 17 00:00:00 2001 From: Kentaro Ohkouchi Date: Fri, 28 Jul 2023 14:23:47 +0900 Subject: [PATCH 3/5] =?UTF-8?q?OAuth=20=E7=AE=A1=E7=90=86=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=A7=20grant=5Ftype=3Dpassword=20=E3=82=92?= =?UTF-8?q?=E9=81=B8=E6=8A=9E=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controller/Admin/OAuthController.php | 14 +++++++++----- Form/Type/Admin/ClientType.php | 24 +++++++++++++++++++----- Resource/locale/messages.en.yaml | 5 +++-- Resource/locale/messages.ja.yaml | 5 +++-- Resource/template/admin/OAuth/edit.twig | 3 --- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/Controller/Admin/OAuthController.php b/Controller/Admin/OAuthController.php index 76c268b..58e4eb7 100644 --- a/Controller/Admin/OAuthController.php +++ b/Controller/Admin/OAuthController.php @@ -108,7 +108,7 @@ public function create(Request $request) $secret = $form->get('secret')->getData(); try { - $client = new Client($name, $identifier, null); // FIXME Set client_secret to null + $client = new Client($name, $identifier, $secret); $client = $this->updateClientFromForm($client, $form); $this->clientManager->save($client); @@ -209,10 +209,14 @@ function (string $redirectUri): RedirectUri { ); $client->setRedirectUris(...$redirectUris); - $grants = [ - new Grant(OAuth2Grants::PASSWORD), - new Grant(OAuth2Grants::REFRESH_TOKEN) - ]; + $grants = array_map( + function (string $grant): Grant { + return new Grant($grant); + }, + $form->get('grants')->getData() + ); + array_push($grants, new Grant(OAuth2Grants::REFRESH_TOKEN)); // RefreshToken は常に利用可能 + $client->setGrants(...$grants); $scopes = array_map( diff --git a/Form/Type/Admin/ClientType.php b/Form/Type/Admin/ClientType.php index 596d50a..affbdb2 100644 --- a/Form/Type/Admin/ClientType.php +++ b/Form/Type/Admin/ClientType.php @@ -14,12 +14,14 @@ namespace Plugin\Api42\Form\Type\Admin; use Eccube\Common\EccubeConfig; +use Eccube\Form\FormError; use Exception; -use Symfony\Component\Form\AbstractType; +use Eccube\Form\FormBuilder; +use Eccube\Form\FormEvent; +use Eccube\Form\Type\AbstractType; +use Eccube\Validator\Constraints as Assert; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints as Assert; use League\Bundle\OAuth2ServerBundle\OAuth2Grants; class ClientType extends AbstractType @@ -45,7 +47,7 @@ public function __construct( * * @throws Exception */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilder $builder, array $options) { $builder ->add('identifier', TextType::class, [ @@ -61,7 +63,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'mapped' => false, 'data' => hash('sha512', random_bytes(32)), 'constraints' => [ - new Assert\NotBlank(), new Assert\Length(['max' => 128]), new Assert\Regex(['pattern' => '/^[0-9a-zA-Z]+$/']), ], @@ -89,6 +90,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ->add('grants', ChoiceType::class, [ 'choices' => [ 'Authorization code' => OAuth2Grants::AUTHORIZATION_CODE, + 'Password' => OAuth2Grants::PASSWORD, ], 'expanded' => true, 'multiple' => true, @@ -98,6 +100,18 @@ public function buildForm(FormBuilderInterface $builder, array $options) new Assert\NotBlank(), ], ]); + + $builder->onPostSubmit(function (FormEvent $event) { + $form = $event->getForm(); + $grants = $form['grants']->getData(); + $secret = $form['secret']->getData(); + + if (in_array(OAuth2Grants::AUTHORIZATION_CODE, $grants) && empty($secret)) { + // ja: Authorization code grant を指定した場合は client_secret を入力してください。 + // en: Please enter client_secret if you specify Authorization code grant. + $form['secret']->addError(new FormError(trans('api.admin.oauth.client_secret.required'))); + } + }); } /** diff --git a/Resource/locale/messages.en.yaml b/Resource/locale/messages.en.yaml index ecf4b99..365dbc0 100644 --- a/Resource/locale/messages.en.yaml +++ b/Resource/locale/messages.en.yaml @@ -15,7 +15,7 @@ api: identifier: Client ID identifier_tooltip: Up to 32 alphanumeric characters secret: Client Secret - secret_tooltip: Up to 128 alphanumeric characters + secret_tooltip: Up to 128 alphanumeric characters, Required if Authorization code grant is specified scope: Scope scope_tooltip: GraphQL Query requires read, Mutation requires write/write Scope scope.read.description: 'Read %shop_name% data' @@ -23,7 +23,7 @@ api: redirect_uri: Redirect URI redirect_uri_tooltip: You can enter multiple URIs separated by commas grant_type: Grant Type - grant_type_tooltip: Supports only authorization code grant + grant_type_tooltip: Authorization code grant and Password grant are supported client_registration: Client Registration delete__confirm_title: Delete a Client delete__confirm_message: Are you sure to delete this Client? @@ -34,6 +34,7 @@ api: allow__confirm_description: 'Allow this app to:' allow: Allow deny: Deny + client_secret.required: 'Please enter client_secret if you specify Authorization code grant.' webhook: management: WebHook registration: WebHook Registration diff --git a/Resource/locale/messages.ja.yaml b/Resource/locale/messages.ja.yaml index fe95c32..7f0645a 100644 --- a/Resource/locale/messages.ja.yaml +++ b/Resource/locale/messages.ja.yaml @@ -15,7 +15,7 @@ api: identifier: クライアントID identifier_tooltip: 32文字以下の半角英数 secret: クライアントシークレット - secret_tooltip: 128文字以下の半角英数 + secret_tooltip: 128文字以下の半角英数。Authorization code grant を指定した場合は必須 scope: スコープ scope_tooltip: GraphQLのQueryにはread, Mutationにはwrite/writeのScopeが必要 scope.read.description: '%shop_name%のデータに対する読み取り' @@ -23,7 +23,7 @@ api: redirect_uri: リダイレクトURI redirect_uri_tooltip: カンマ区切りで複数のURIを入力可能 grant_type: グラントタイプ - grant_type_tooltip: Authorization code grantのみサポート + grant_type_tooltip: Authorization code grant 及び Password grant に対応 client_registration: OAuthクライアント登録 delete__confirm_title: OAuthクライアントを削除します。 delete__confirm_message: OAuthクライアントを削除してよろしいですか? @@ -34,6 +34,7 @@ api: allow__confirm_description: 'このアプリに以下を許可します:' allow: 許可する deny: 許可しない + client_secret.required: 'Authorization code grant を指定した場合は client_secret を入力してください。' webhook: management: WebHook管理 registration: WebHook登録 diff --git a/Resource/template/admin/OAuth/edit.twig b/Resource/template/admin/OAuth/edit.twig index e9c19ca..f14eb97 100644 --- a/Resource/template/admin/OAuth/edit.twig +++ b/Resource/template/admin/OAuth/edit.twig @@ -61,9 +61,6 @@ file that was distributed with this source code.
From 563ad6ccfa6f34573bb2338e0014131905346939 Mon Sep 17 00:00:00 2001 From: Kentaro Ohkouchi Date: Mon, 31 Jul 2023 13:39:27 +0900 Subject: [PATCH 4/5] Create OAuth2 client dynamically --- .github/workflows/main.yml | 1 - .../AuthorizationControllerTest.php | 34 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b90720f..5c16f9b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,7 +150,6 @@ jobs: bin/console eccube:plugin:enable --code=${PLUGIN_CODE} bin/console doctrine:schema:update --force --dump-sql bin/console cache:clear --no-warmup - bin/console league:oauth2-server:create-client --redirect-uri=http://127.0.0.1:8000/ --grant-type=authorization_code --grant-type=client_credentials --grant-type=implicit --grant-type=password --grant-type=refresh_token --scope=read --scope=write test rm codeception/_support/*Tester.php chmod 600 app/PluginData/Api42/oauth/private.key diff --git a/Tests/Web/Admin/OAuth2Bundle/AuthorizationControllerTest.php b/Tests/Web/Admin/OAuth2Bundle/AuthorizationControllerTest.php index 1f04625..7e5cd1c 100644 --- a/Tests/Web/Admin/OAuth2Bundle/AuthorizationControllerTest.php +++ b/Tests/Web/Admin/OAuth2Bundle/AuthorizationControllerTest.php @@ -15,20 +15,29 @@ use Eccube\Common\Constant; use Eccube\Tests\Web\Admin\AbstractAdminWebTestCase; +use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\Grant; +use League\Bundle\OAuth2ServerBundle\Model\RedirectUri; +use League\Bundle\OAuth2ServerBundle\Model\Scope; +use League\Bundle\OAuth2ServerBundle\OAuth2Grants; use Symfony\Component\HttpFoundation\Response; use League\Bundle\OAuth2ServerBundle\Model\Client; class AuthorizationControllerTest extends AbstractAdminWebTestCase { + /** @var Client */ + private $OAuth2Client; + public function setUp(): void { parent::setUp(); + $this->OAuth2Client = $this->createOAuth2Client(); } public function testRoutingAdminOauth2Authorize_ログインしている場合は権限移譲確認画面を表示() { /** @var Client $Client */ - $Client = $this->entityManager->getRepository(Client::class)->findOneBy([]); + $Client = $this->OAuth2Client; $this->client->request('GET', $this->generateUrl( @@ -53,7 +62,7 @@ public function testRoutingAdminOauth2Authorize_ログインしている場合 public function testRoutingAdminOauth2Authorize_権限移譲を許可() { /** @var Client $Client */ - $Client = $this->entityManager->getRepository(Client::class)->findOneBy([]); + $Client = $this->OAuth2Client; $authorize_url = $this->generateUrl( 'oauth2_authorize', [ @@ -98,7 +107,7 @@ public function testRoutingAdminOauth2Authorize_権限移譲を許可() public function testRoutingAdminOauth2Authorize_権限移譲を許可しない() { /** @var Client $Client */ - $Client = $this->entityManager->getRepository(Client::class)->findOneBy([]); + $Client = $this->OAuth2Client; $authorize_url = $this->generateUrl( 'oauth2_authorize', [ @@ -171,4 +180,23 @@ private function parseCallbackParams(Response $response) return $redirectParams; } + + private function createOAuth2Client(): Client + { + $client_id = hash('md5', random_bytes(16)); + $client_secret = hash('sha256', random_bytes(32)); + $Client = new Client('', $client_id, $client_secret); + $Client + ->setScopes(new Scope('read')) + ->setRedirectUris(new RedirectUri('http://127.0.0.1:8000/')) + ->setGrants( + new Grant(OAuth2Grants::AUTHORIZATION_CODE), + new Grant(OAuth2Grants::REFRESH_TOKEN) + ) + ->setActive(true); + $clientManager = self::$container->get(ClientManagerInterface::class); + $clientManager->save($Client); + + return $Client; + } } From 2454d58b204af5cf8e683e204412ce94dd561aba Mon Sep 17 00:00:00 2001 From: Kentaro Ohkouchi Date: Mon, 31 Jul 2023 13:42:03 +0900 Subject: [PATCH 5/5] Add TokenControllerWithROPCTest --- .../TokenControllerWithROPCTest.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 Tests/Web/OAuth2Bundle/TokenControllerWithROPCTest.php diff --git a/Tests/Web/OAuth2Bundle/TokenControllerWithROPCTest.php b/Tests/Web/OAuth2Bundle/TokenControllerWithROPCTest.php new file mode 100644 index 0000000..4748d3c --- /dev/null +++ b/Tests/Web/OAuth2Bundle/TokenControllerWithROPCTest.php @@ -0,0 +1,91 @@ +OAuth2Client = $this->createOAuth2Client(); + $this->Customer = $this->createCustomer(); + } + + public function testGetInstance() + { + $this->client->request( + 'POST', + $this->generateUrl('oauth2_token'), + [ + 'grant_type' => OAuth2Grants::PASSWORD, + 'client_id' => $this->OAuth2Client->getIdentifier(), + 'username' => $this->Customer->getEmail(), + 'password' => 'password', + 'scope' => 'read write' + ] + ); + + self::assertSame(200, $this->client->getResponse()->getStatusCode()); + $response = json_decode($this->client->getResponse()->getContent(), true); + self::assertArrayHasKey('access_token', $response); + self::assertArrayHasKey('refresh_token', $response); + self::assertSame('Bearer', $response['token_type']); + self::assertSame(3600, $response['expires_in']); + + $parser = new Parser(new JoseEncoder()); + $token = $parser->parse($response['access_token']); + + self::assertTrue($token->isRelatedTo($this->Customer->getEmail()), 'Token is not related to customer(sub)'); + self::assertFalse($token->isExpired(new \DateTimeImmutable('+3590 second')), 'Token is expired(exp)'); + self::assertTrue($token->isPermittedFor($this->OAuth2Client->getIdentifier()), 'Token is not permitted for client(aud)'); + self::assertTrue($token->hasBeenIssuedBefore(new \DateTimeImmutable('+1 second')), 'Token has not been issued before(iat)'); + self::assertTrue($token->isMinimumTimeBefore(new \DateTimeImmutable('+1 second')), 'Token is not minimum time before(nbf)'); + } + + protected function createOAuth2Client(): Client + { + $client_id = hash('md5', random_bytes(16)); + $Client = new Client('', $client_id, null); // public client + $Client + ->setScopes( + new Scope('read'), + new Scope('write') + ) + ->setRedirectUris(new RedirectUri('http://127.0.0.1:8000/')) + ->setGrants( + new Grant(OAuth2Grants::PASSWORD), + new Grant(OAuth2Grants::REFRESH_TOKEN) + ) + ->setActive(true); + + $clientManager = self::$container->get(ClientManagerInterface::class); + $clientManager->save($Client); + + return $Client; + } +}