Skip to content

Commit

Permalink
Merge pull request #144 from nanasess/password-grant
Browse files Browse the repository at this point in the history
Support grant_type=password
  • Loading branch information
kiy0taka authored Aug 1, 2023
2 parents 40a5f82 + 2454d58 commit 2aab400
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 31 deletions.
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;
}
}

0 comments on commit 2aab400

Please sign in to comment.