読者です 読者をやめる 読者になる 読者になる

HWIOAuthBundleを使ってSymfony2でOAuth認証を実装する(EntityUserProvider)

HWIOAuthBundleを使ってSymfony2でOAuth認証を実装するの続編です。

今回はアプリ側にユーザー情報を格納するテーブルがあることを前提に、OAuth認証によって得られた情報と連携させてみます。

実現したい内容としては、以下となります。

  1. ログイン用のリンクをクリック
  2. Google側でログイン
  3. アプリ側のユーザー情報を取得
    • ユーザー情報が存在すればそれを利用して認証する
    • もしユーザー情報が存在しなければOAuth認証によって得られた情報をもとにユーザーを作成する

すべてHWIOAuthBundleで実現できれば良いのですが、「ユーザー情報が存在しなければ~」はどうやらできないみたい(参考:Support connect without registration)なのでカスタマイズが必要になります。

1. Entityおよびテーブルを作成

1.1. Entityを作成
php app/console generate:entity --entity=VendorSampleBundle:User

カラムは以下を設定しました。

gid string 255 Google側との連携用カラム
name string 255 ユーザー名
email string 255 メールアドレス
is_active boolean - 有効/無効

作成後、HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserを継承します。

<?php

namespace Vendor\SampleBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser;

/**
 * User
 */
class User extends OAuthUser
{
    // ...
}
1.2. テーブルを作成
php app/console doctrine:schema:create

2. OAuthUserProviderを作成し、サービスへ登録

2.1. OAuthUserProviderを作成

前回はHWIOAuthBundleが標準で提供しているOAuthUserProviderを使用しました。
今回は先ほど作ったユーザー情報を格納するテーブルと連携を行うため、EntityUserProviderを継承して新たにsrc\Vendor\SampleBundle\Auth\OAuthUserProvider.phpを作成します。

<?php
namespace Vendor\SampleBundle\Auth;

use HWI\Bundle\OAuthBundle\Security\Core\User\EntityUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class OAuthUserProvider extends EntityUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
    }

    public function refreshUser(UserInterface $user)
    {
    }

    public function supportsClass($class)
    {
    }
}

中身は後で実装します。

2.2. OAuthUserProviderをService Containerに登録

src\Vendor\SampleBundle\Resources\config\services.ymlを以下のように書き換えます。

変更前

parameters:
    vendor_sample.oauth_user_provider.class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider

services:
    vendor_sample.oauth_user_provider.service:
        class: %vendor_sample.oauth_user_provider.class%

変更後

parameters:
    vendor_sample.oauth_user_provider.class: Vendor\SampleBundle\Auth\OAuthUserProvider
    vendor_sample.entity.user.class: Vendor\SampleBundle\Entity\User

services:
    vendor_sample.oauth_user_provider.service:
        class: %vendor_sample.oauth_user_provider.class%
        arguments: [@doctrine, %vendor_sample.entity.user.class%, {google: gid}]

OAuthUserProviderインスタンスを生成する際に、親クラスEntityUserProviderコンストラクタ引数に定義されている

  • DBコネクション
  • 利用するUser Entitiyのクラス名
  • OAuth認証との連携用プロパティ

の3つをInjectするように設定します。

3. OAuthUserProviderを実装

を参考にしながら実装していきます。

3.1. loadUserByUsername

本来であれば認証用フォーム等から入力されたユーザー名をもとに該当するEntityを返す処理を書くのだろうが、HWIOAuthBundleによってこいつの代わりにEntityUserProviderloadUserByOAuthUserResponseが利用されるようになるため、実装不要。

たぶん。

念の為確認してみたが、一度も呼ばれなかった。

3.2. refreshUser

認証が必要な箇所へのアクセス毎に呼ばれる。
DBからユーザー情報を取得しなおしたり、内部的な処理を行ったり(最終アクセス時刻の記録など?)する場合は実装するらしい。
今回は特にその必要もないので、そのまま返す。

<?php
    public function refreshUser(UserInterface $user)
    {
        return $user;
    }
3.3. supportsClass

supportsClassによると、引数で与えられたユーザークラスをこのUserProviderがサポートするかどうかを返せば良いらしいので、以下のように実装。
いつ、どのような場面で呼ばれるのか不明。

<?php
    public function supportsClass($class)
    {
        return 'Vendor\\SampleBundle\\Entitiy\\User' === $class;
    }
3.4. コンストラクタ、loadUserByOAuthUserResponseをOverride

loadUserByOAuthUserResponseの標準実装は次のようになっています。

<?php
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        $resourceOwnerName = $response->getResourceOwner()->getName();

        if (!isset($this->properties[$resourceOwnerName])) {
            throw new \RuntimeException(sprintf("No property defined for entity for resource owner '%s'.", $resourceOwnerName));
        }

        $username = $response->getUsername();
        $user = $this->repository->findOneBy(array($this->properties[$resourceOwnerName] => $username));

        if (null === $user) {
            throw new UsernameNotFoundException(sprintf("User '%s' not found.", $username));
        }

        return $user;
    }

冒頭に書いた、

  • ユーザー情報が存在すればそれを利用して認証する
  • もしユーザー情報が存在しなければOAuth認証によって得られた情報をもとにユーザーを作成する

のうち、前者しか実現できていないのがわかると思います。

なので、loadUserByOAuthUserResponseをOverrideします。
作成したユーザーを永続化するためにEntityManagerを使いたいので、以下のようにコンストラクタをOverrideします。

<?php
    protected $em;

    public function __construct(ManagerRegistry $registry, $class, array $properties, $managerName = null)
    {
        parent::__construct($registry, $class, $properties, $managerName);
        $this->em = $registry->getManager($managerName);
    }

次にloadUserByOAuthUserResponse

<?php
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        try {
            return parent::loadUserByOAuthUserResponse($response);
        } catch (UsernameNotFoundException $e) {
            $rawResponse = $response->getResponse();

            $user = new User($rawResponse['name']);
            $user->setGid($rawResponse['id']);
            $user->setEmail($rawResponse['email']);
            $user->setIsActive(true);
            $this->em->persist($user);
            $this->em->flush();

            return $user;
        }
    }

これで、後者についても実現できます。

最後に

「ユーザー情報が存在しなければ~」を自前で実装しましたが、FOSUserBundleと一緒に使うとひょっとしたらすべて面倒見てくれるかもしれません。

また。今回の私の例では必要なかったので調べていませんが、Reference configurationを見るに、OAuth認証が完了した後にアプリ側に用意した登録フォームでアプリ独自の設定情報をユーザーに登録させるための"connect"という仕組みも提供しているようです。


やっぱり日本語情報が少ないですね。。。

以上です。