【EC-CUBE4】Yahoo!ID連携を実装する方法

EC-CUBE4でYahoo!ID連携を実装する方法です。

細かい説明はしていませんのでご了承ください。

ファイル設置場所やファイル名はネームスペースやクラス名をご確認ください。

必要なライブラリをComposerでインストール

bin/console eccube:composer:require knpuniversity/oauth2-client-bundle

 

bundles.phpにライブラリを追加

<?php
// app/config/eccube/bundles.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.
 */

return [
   // ...
    KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true]
];

 

YConnectプロバイダーを作成

<?php


namespace Customize\Security\OAuth2\Client\Provider\Yahoo;


use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;

class YConnect extends AbstractProvider
{
    use BearerAuthorizationTrait;

    /**
     * @inheritDoc
     */
    public function getBaseAuthorizationUrl()
    {
        return 'https://auth.login.yahoo.co.jp/yconnect/v2/authorization';
    }

    /**
     * @inheritDoc
     */
    public function getBaseAccessTokenUrl(array $params)
    {
        return 'https://auth.login.yahoo.co.jp/yconnect/v2/token';
    }

    /**
     * @inheritDoc
     */
    protected function getAccessTokenOptions(array $params)
    {
        $options = parent::getAccessTokenOptions([
            'grant_type' => 'authorization_code',
            'code' => $params['code'],
            'redirect_uri' => $params['redirect_uri']
        ]);

        $options['headers']['Authorization'] = 'Basic '.base64_encode($params['client_id'].':'.$params['client_secret']);
        return $options;
    }

    /**
     * @inheritDoc
     */
    public function getResourceOwnerDetailsUrl(AccessToken $token)
    {
        return 'https://userinfo.yahooapis.jp/yconnect/v2/attribute';
    }

    /**
     * @inheritDoc
     */
    protected function getDefaultScopes()
    {
        return [
            'openid'
        ];
    }

    /**
     * @inheritDoc
     */
    protected function checkResponse(ResponseInterface $response, $data)
    {
        if(isset($data['error'])) {
            $message = $data['error'];

            if(isset($data['error_description'])) {
                $message .= ":".$data['error_description'];
            }

            throw new IdentityProviderException($message, $response->getStatusCode(), $data);
        }
    }

    /**
     * @inheritDoc
     */
    protected function createResourceOwner(array $response, AccessToken $token)
    {
        return new YConnectResourceOwner($response);
    }
}

 

ユーザー情報を取得するYConnectResourceOwnerを作成

<?php


namespace Customize\Security\OAuth2\Client\Provider\Yahoo;


use League\OAuth2\Client\Provider\ResourceOwnerInterface;

class YConnectResourceOwner implements ResourceOwnerInterface
{
    /**
     * @var array
     */
    protected $response;

    public function __construct(array $response = [])
    {
        $this->response = $response;
    }

    public function __call($name, $arguments)
    {
        $get = substr($name,0, 3);
        if ($get !== 'get') {
            return null;
        }
        $parameter = function($name) {
            return ltrim(strtolower(preg_replace('/[A-Z]/', '_\0', str_replace("get", "", $name))), '_');
        };
        return $this->getResource($parameter($name));
    }

    /**
     * @inheritDoc
     */
    public function getId()
    {
        return $this->getResource("sub");
    }

    /**
     * @inheritDoc
     */
    public function toArray()
    {
        return $this->response;
    }

    protected function getResource(string $name)
    {
        return isset($this->response[$name]) ? $this->response[$name] : null;
    }
}

 

 

knpu_oauth2_client.yamlを追加

YahooJapan!デベロッパーネットワークのアプリケーションの管理にてClient IDとシークレットを取得して設定してください。

 

knpu_oauth2_client:
  clients:
    yconnect_client:
      type: generic
      provider_class: Customize\Security\OAuth2\Client\Provider\Yahoo\YConnect
      client_id: *****************************************
      client_secret: *************************************
      redirect_route: yahoo_callback

 

 

Customerエンティティにyahoo_user_idプロパティを追加

<?php

namespace Customize\Entity;

use Customize\Entity\Master\CustomerType;
use Doctrine\ORM\Mapping as ORM;
use Eccube\Annotation\EntityExtension;

/**
* @EntityExtension("Eccube\Entity\Customer")
*/
trait CustomerTrait
{
    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $yahoo_user_id;

    public function getYahooUserId(): ?string
    {
        return $this->yahoo_user_id;
    }

    public function setYahooUserId(?string $yahoo_user_id): self
    {
        $this->yahoo_user_id = $yahoo_user_id;

        return $this;
    }
}

 

 

YahooAuthenticatorを用意

<?php


namespace Customize\Security\Authenticator;


use Doctrine\ORM\EntityManagerInterface;
use Eccube\Entity\Customer;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Security\Exception\FinishRegistrationException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class YahooAuthenticator extends SocialAuthenticator
{
    const OAUTH2_PROVIDER = "yahoo";

    /**
     * @var ClientRegistry
     */
    private $clientRegistry;

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var RouterInterface
     */
    private $router;

    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    public function __construct(
        ClientRegistry $clientRegistry,
        EntityManagerInterface $entityManager,
        RouterInterface $router,
        TokenStorageInterface $tokenStorage
    )
    {
        $this->clientRegistry = $clientRegistry;
        $this->entityManager = $entityManager;
        $this->router = $router;
        $this->tokenStorage = $tokenStorage;
    }

    public function supports(Request $request)
    {
        return $request->attributes->get('_route') === 'yahoo_callback';
    }

    /**
     * @inheritDoc
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse(
            $this->router->generate("yahoo"),
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }

    /**
     * @inheritDoc
     */
    public function getCredentials(Request $request)
    {
        try {
            return $this->fetchAccessToken($this->getYahooClient());
        } catch (AuthenticationException $e) {
            throw new $e;
        }

    }

    /**
     * @inheritDoc
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $userInfo = $this->getYahooClient()
            ->fetchUserFromToken($credentials);

        // Yahooでメールアドレス認証していない場合がある
        if (!$userInfo->getEmailVerified()) {
            throw new IdentityProviderException('Yahooでメールアドレスが認証されていません。');
        }

        // ヤフー連携済みの場合
        $Customer = $this->entityManager->getRepository(Customer::class)
            ->findOneBy(['yahoo_user_id' => $userInfo->getId()]);
        if ($Customer) {
            return $Customer;
        }

        $Customer = $this->entityManager->getRepository(Customer::class)
            ->findOneBy(['email' => $userInfo->getEmail()]);

        // 会員登録していない場合、会員登録ページへ
        if (!$Customer) {
            throw new FinishRegistrationException(array_merge($userInfo->toArray(), ["provider" => self::OAUTH2_PROVIDER]));
        }

        // 通常の会員登録済みの場合はユーザー識別子を保存
        $Customer->setYahooUserId($userInfo->getId());
        $this->entityManager->persist($Customer);
        $this->entityManager->flush();

        return $Customer;
    }

    /**
     * @inheritDoc
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        // 会員登録していない場合
        if ($exception instanceof FinishRegistrationException) {
            $this->saveUserInfoToSession($request, $exception);
            return new RedirectResponse($this->router->generate("entry"));
        } else {
            $this->saveAuthenticationErrorToSession($request, $exception);
            return new RedirectResponse($this->router->generate("mypage_login"));
        }
    }

    /**
     * @inheritDoc
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $targetUrl = $this->router->generate("mypage");

        return new RedirectResponse($targetUrl);

    }

    /**
     * EC-CUBEがUsernamePasswordTokenなので合わせる
     *
     * @param UserInterface $user
     * @param string $providerKey
     * @return UsernamePasswordToken|\Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
     */
    public function createAuthenticatedToken(UserInterface $user, $providerKey)
    {
        if ($user instanceof Customer && $providerKey === 'customer') {
            return new UsernamePasswordToken($user, null, $providerKey, ['ROLE_USER']);
        }

        return parent::createAuthenticatedToken($user, $providerKey);
    }

    private function getYahooClient()
    {
        return $this->clientRegistry
            ->getClient('yconnect_client');
    }
}

 

security.yamlを修正

guardを追加しています。

security:
    encoders:
        # Our user class and the algorithm we'll use to encode passwords
        # https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password
        Eccube\Entity\Member:
          id: Eccube\Security\Core\Encoder\PasswordEncoder
        Eccube\Entity\Customer:
          id: Eccube\Security\Core\Encoder\PasswordEncoder
    providers:
        # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
        # In this example, users are stored via Doctrine in the database
        # To see the users at src/App/DataFixtures/ORM/LoadFixtures.php
        # To load users from somewhere else: https://symfony.com/doc/current/security/custom_provider.html
        member_provider:
            id: Eccube\Security\Core\User\MemberProvider
        customer_provider:
            id: Eccube\Security\Core\User\CustomerProvider
    # https://symfony.com/doc/current/security.html#initial-security-yml-setup-authentication
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            pattern: '^/%eccube_admin_route%/'
            anonymous: true
            provider: member_provider
            form_login:
                check_path: admin_login
                login_path: admin_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: admin_homepage
                username_parameter: 'login_id'
                password_parameter: 'password'
                use_forward: true
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
            logout:
                path: admin_logout
                target: admin_login
        customer:
            pattern: ^/
            anonymous: true
            provider: customer_provider
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 3600
                name: eccube_remember_me
                remember_me_parameter: 'login_memory'
            form_login:
                check_path: mypage_login
                login_path: mypage_login
                csrf_token_generator: security.csrf.token_manager
                default_target_path: homepage
                username_parameter: 'login_email'
                password_parameter: 'login_pass'
                use_forward: true
                success_handler: eccube.security.success_handler
                failure_handler: eccube.security.failure_handler
            logout:
                path: logout
                target: homepage
            guard:
                authenticators:
                    - Customize\Security\Authenticator\YahooAuthenticator

    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

 

Yahoo!ID連携用のコントローラーを用意

<?php


namespace Customize\Controller;


use Eccube\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * @Route("/yahoo")
 *
 * Class YahooController
 * @package Customize\Controller
 */
class YahooController extends AbstractController
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    public function __construct(
        TokenStorageInterface $tokenStorage
    )
    {
        $this->tokenStorage = $tokenStorage;
    }

    /**
     * @Route("/", name="yahoo")
     *
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function index()
    {
        return $this->get('oauth2.registry')
            ->getClient('yconnect_client')
            ->redirect([
                "scope" => "openid profile email address"
            ]);
    }

    /**
     * @Route("/callback", name="yahoo_callback")
     */
    public function callback()
    {
        if($this->isGranted("IS_AUTHENTICATED_FULLY")) {
            return $this->redirectToRoute("mypage");
        }else{
            return $this->redirectToRoute("yahoo");
        }
    }
}

 

EntryTypeを拡張

Yahoo!ID連携経由で会員登録する場合はyahoo_user_idを登録するためEntryTypeを拡張します。

 

<?php

namespace Customize\Form\Extension;

use Customize\Security\Authenticator\YahooAuthenticator;
use Eccube\Entity\Customer;
use Eccube\Form\Type\AddressType;
use Eccube\Form\Type\Front\EntryType;
use Eccube\Form\Type\NameType;
use Eccube\Form\Type\PostalType;
use Eccube\Form\Type\RepeatedEmailType;
use Eccube\Repository\Master\PrefRepository;
use KnpU\OAuth2ClientBundle\Security\Helper\FinishRegistrationBehavior;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\RequestStack;

class EntryTypeExtension extends AbstractTypeExtension
{
    use FinishRegistrationBehavior;

    /**
     * @var RequestStack
     */
    private $requestStack;

    /**
     * @var PrefRepository
     */
    private $prefRepository;

    public function __construct(
        RequestStack $requestStack,
        PrefRepository $prefRepository
    ) {
        $this->requestStack = $requestStack;
        $this->prefRepository = $prefRepository;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $userInfo = $this->getUserInfoFromSession($this->requestStack->getCurrentRequest());
        if($userInfo && $userInfo["provider"] === YahooAuthenticator::OAUTH2_PROVIDER) {
            // メールアドレスをセット
            $builder ->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) use ($userInfo) {
                    $form = $event->getForm(); 
                    $form['email']->setData($userInfo["email"]); 
                });

            // ユーザー識別子をCustomerにセット
            $builder
                ->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) use ($userInfo) {
                    $Customer = $event->getData();
                    if($Customer instanceof Customer) {
                        $Customer->setYahooUserId($userInfo["sub"]);
                    }
                });
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getExtendedType()
    {
        return EntryType::class;
    }
}

 

ソーシャルログインのときは会員登録ページでメールアドレスを編集不可にする

Entry/index.twigを編集してください。

{% if app.session.get('guard.finish_registration.user_information') %}
                        <dl>
                            <dt>
                                {{ form_label(form.email, 'common.mail_address', {'label_attr': {'class': 'ec-label'}}) }}
                            </dt>
                            <dd>
                                {{ form.email.vars.data }}
                                {{ form_widget(form.email.first, { type : 'hidden' }) }}
                                {{ form_widget(form.email.second, { type : 'hidden' }) }}
                            </dd>
                        </dl>
{% else %}
                        <dl>
                            <dt>
                                {{ form_label(form.email, 'common.mail_address', { 'label_attr': { 'class': 'ec-label' }}) }}
                            </dt>
                            <dd>
                                <div class="ec-input{{ has_errors(form.email.first) ? ' error' }}">
                                    {{ form_widget(form.email.first, { 'attr': { 'placeholder': 'common.mail_address_sample' }}) }}
                                    {{ form_errors(form.email.first) }}
                                </div>
                                <div class="ec-input{{ has_errors(form.email.second) ? ' error' }}">
                                    {{ form_widget(form.email.second, { 'attr': { 'placeholder': 'common.repeated_confirm' }}) }}
                                    {{ form_errors(form.email.second) }}
                                </div>
                            </dd>
                        </dl>
{% endif %}

 

 

以上で完成のはずです。

 

Facebookログインなども上記を参考に修正すれば実装できるとかと思います。

お気軽にコメントをどうぞ

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください