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

Support grant_type=password #144

Merged
merged 5 commits into from
Aug 1, 2023
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
1 change: 0 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 2 additions & 4 deletions Controller/Admin/OAuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,8 @@ function (string $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));
}
array_push($grants, new Grant(OAuth2Grants::REFRESH_TOKEN)); // RefreshToken は常に利用可能

$client->setGrants(...$grants);

$scopes = array_map(
Expand Down
30 changes: 21 additions & 9 deletions EventListener/UserResolveListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,36 @@

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
*/
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;
}

Expand All @@ -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())) {
Expand Down
24 changes: 19 additions & 5 deletions Form/Type/Admin/ClientType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, [
Expand All @@ -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]+$/']),
],
Expand Down Expand Up @@ -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,
Expand All @@ -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')));
}
});
}

/**
Expand Down
7 changes: 5 additions & 2 deletions Resource/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,8 +39,11 @@ 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\CustomerProvider'
- '@Eccube\Security\Core\User\MemberProvider'
- '@Eccube\Security\Core\User\UserPasswordHasher'
tags:
Expand Down
5 changes: 3 additions & 2 deletions Resource/locale/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ 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'
scope.write.description: 'Write %shop_name% data'
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?
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Resource/locale/messages.ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ 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%のデータに対する読み取り'
scope.write.description: '%shop_name%のデータに対する書き込み'
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クライアントを削除してよろしいですか?
Expand All @@ -34,6 +34,7 @@ api:
allow__confirm_description: 'このアプリに以下を許可します:'
allow: 許可する
deny: 許可しない
client_secret.required: 'Authorization code grant を指定した場合は client_secret を入力してください。'
webhook:
management: WebHook管理
registration: WebHook登録
Expand Down
3 changes: 0 additions & 3 deletions Resource/template/admin/OAuth/edit.twig
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ file that was distributed with this source code.
<label class="col-form-label" data-tooltip="true" data-placement="top" title="{{ 'api.admin.oauth.secret_tooltip'|trans }}">
<span>{{ 'api.admin.oauth.secret'|trans }}</span>
<i class="fa fa-question-circle fa-lg ms-1"></i>
<span class="badge bg-primary ms-1">
{{ 'admin.common.required'|trans }}
</span>
</label>
</div>
<div class="col">
Expand Down
34 changes: 31 additions & 3 deletions Tests/Web/Admin/OAuth2Bundle/AuthorizationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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',
[
Expand Down Expand Up @@ -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',
[
Expand Down Expand Up @@ -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;
}
}
91 changes: 91 additions & 0 deletions Tests/Web/OAuth2Bundle/TokenControllerWithROPCTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Plugin\Api42\Tests\Web\OAuth2Bundle;

use Eccube\Entity\Customer;
use Eccube\Tests\Web\AbstractWebTestCase;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token\Parser;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\Client;
use League\Bundle\OAuth2ServerBundle\Model\Grant;
use League\Bundle\OAuth2ServerBundle\Model\RedirectUri;
use League\Bundle\OAuth2ServerBundle\Model\Scope;
use League\Bundle\OAuth2ServerBundle\OAuth2Grants;

class TokenControllerWithROPCTest extends AbstractWebTestCase
{
protected ?Client $OAuth2Client;
protected ?Customer $Customer;

public function setUp(): void
{
parent::setUp();
$this->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;
}
}