Symfony2 Microsoft Live oAuth 2.0

Реализация oAuth 2.0 авторизации с использованием бандла HWIOAuthBundle

Принципы

Авторизация oAuth - процесс проверки существования пользователя на oAuth-сервере, в результате чего мы получаем только UserName (Логин или левую часть емэйл-адреса пользователя) и при желании др. информацию о пользователе.

Провайдер - это прослойка, т.е. нам понадобится написать свой класс (прослойку) через который будет выполняться Авторизация (не Аутентификация) пользователя (по UserName).

В книге рецептов Symfony2 есть пример реализации своего провайдера » Следуя этому примеру и будем писать провайдер, который для авторизации будет использовать только UserName, как я уже и писал выше.

token - некий рэндомно сгенерированный ключ, с помощью которого происходит доверие и общение oAuth-сервера (например Facebook) и oAuth-клиента (Вашего сайта).

Symfony2 Microsoft Live oAuth 2.0

Устанавливаем HWIOAuthBundle

Первым делом конечно устанавливаем бандл, как установить написано на странице https://github.com/hwi/HWIOAuthBundle читай параграф Installation

Теперь создадим свое приложение на сайте live.com

Заходите на страницу управления приложениями ( если нужно авторизуйтесь или зарегистрируйте себе учетную запись в live.com ).

Теперь создавайте свое приложение кликнув по ссылке "Create application". Обращаю внимание, что поле "Redirect domain:" нужно заполнить например так: http://yapro.ru/

В результате, у Вас будут 2 нужных Вам параметра:

client_id: 0000000045704B0E
client_secret: lofQ9aZXCZXCZXCZXC18gk-0dDAXp


И прежде чем приступить к рутинной работе, для самых опытных выкладываю diff.patch всего ниже перечисленного.

Конфигурируем бандл

В конце файла /app/config/config.yml пропишите следующее:

# настройки чтобы работал oAuth-бандл HWIOAuthBundle
hwi_oauth:
    # name of the firewall in which this bundle is active, this setting MUST be set
    firewall_name: main

    # an optional setting to configure a query string parameter which can be used to redirect
    # the user after authentication, e.g. /connect/facebook?_destination=/my/destination will
    # redirect the user to /my/destination after facebook authenticates them.  If this is not
    # set then the user will be redirected to the original resource that they requested, or
    # the base address if no resource was requested.  This is similar to the behaviour of
    # [target_path_parameter for form login](http://symfony.com/doc/2.0/cookbook/security/form_login.html).
    # target_path_parameter: _destination

    # here you will add one (or more) configurations for resource owners
    # and other settings you want to adjust in this bundle, just checkout the list below!
    # пример пхп-реализации Microsoft Live OAuth можно посмотреть на: http://wcoders.com/lessons/PHP/137
    resource_owners:
#     владелец ресурса (название, придумал сам)
        owner_windows_live:
#         тип ресурса ( подробнее http://msdn.microsoft.com/ru-ru/library/live/hh243647.aspx )
            type:                windows_live
#         ИД клиента ( ИД приложения )
            client_id:           0000000045704B0E
#         Секрет клиента (версия 1)
            client_secret:       lofQ9aZXCZXCZXCZXC18gk-0dDAXp
#         Вид информации о пользователе, запрашиваемой у сервиса windows_live (мультиварианты: wl.basic wl.emails wl.birthday wl.signin)
#         Другими словами это набор прав доступа. Отметьте действия, которые будут доступны приложению после получения токена.
            scope:               wl.signin
#            response_type - процедура авторизации, по умолчанию: code
#            response_type:       token

Пишем свой провайдер

Создайте файл /src/Acme/DemoBundle/Security/Provider.php со следующим содержимым:

<?php
/**
 * Провайдер аутентификации (не Авторизации) - возвращает менеджеру ( Symfony AuthenticationManager ), а тот, в свою
 * очередь listener’y авторизованный (или не авторизванный) token. На данном этапе задействуется UserProvider, который
 * и работает с данными о пользователе.
 */
namespace Acme\DemoBundle\Security;

use Symfony\Component\Security\Core\User\UserInterface;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Acme\DemoBundle\Entity\User;

class Provider extends OAuthUserProvider
{

    private$doctrine;

    publicfunction__construct($doctrine)
    {
        $this->doctrine=$doctrine;
    }

    /**
     * находит и возвращает экземпляр класса User или пустой массив (а по версии Symfony должен возвращать false).
     * @param string $username - ID пользователя
     * @return User|\Symfony\Component\Security\Core\User\UserInterface
     * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
     */
    publicfunction loadUserByUsername($username)
    {
        if(empty($username)){
            return;
        }
        // получаем данные о пользователе
        /** @var $user \Acme\DemoBundle\Entity\User */
        $user=$this->getUserByWindowsLive($username);

        if($user&&$user->getId()){
            $user->setPassword(sha1($username));
            return$user;
        }

        thrownew UsernameNotFoundException(sprintf('Username "%s" does not exist.',$username));
    }

    /**
     * Предположительно: метод вызывается, когда процесс Аутентификации (не Авторизации) успешно выполнен
     * проверяет сущестование пользователя в Б.Д., заводит сессию и возвращает данные пользователя по $username
     * @param UserResponseInterface $response
     * @return User|\Symfony\Component\Security\Core\User\UserInterface
     * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
     */
    publicfunction loadUserByOAuthUserResponse(UserResponseInterface $response)
    {

        $email=$response->getEmail();// емэйл пользователя, например:totx@narod.ru
        $name=$response->getRealName();// имя пользователя на стороне oAuth-сервера, например:Nikolay Lebedenko
        $username=$response->getUsername();// уникальный ID пользователя на стороне oAuth-сервера, например:8d86a051742940e3
        $response->getAccessToken();// токен (уникальный идентификатор) для авторизации, например:ZxC1/2+3 (более 255 символов)
        $response->getExpiresIn();// предположительно: через какое время токен становится недействительным, например:3600 (секунд)
        $response->getProfilePicture();// изображение профиля, может не быть, например:пусто
        $response->getRefreshToken();// например:пусто
        $response->getTokenSecret();// например:пусто

        if(empty($email)){

            thrownew UsernameNotFoundException('Вы не идентифицированы т.к. не получен Email-адрес');

        }

        $user=$this->getUserByWindowsLive($username);// находим пользователя
        /** @var $user \Acme\DemoBundle\Entity\User */

        // если пользователя нет в базе данных - добавим его
        if(!$user||!$user->getId()){

            $user=new User();
            $user->setName($name);
            $user->setEmail($email);
            $user->setWindowsLive($username);

            $this->doctrine->getManager()->persist($user);
            $this->doctrine->getManager()->flush();

            $user_id=$user->getId();
        }else{
            $user_id=$user->getId();
        }

        if(!$user_id){
            thrownew UsernameNotFoundException('Возникла проблема добавления или определения пользователя');
        }

        return$this->loadUserByUsername($username);

    }

    publicfunction refreshUser(UserInterface $user)
    {
        if(!$userinstanceof User){
            thrownew UnsupportedUserException(sprintf('Instances of "%s" are not supported.',get_class($user)));
        }
        return$this->loadUserByUsername($user->getUsername());
    }

    /**
     * Метод проверки класса пользователя
     * предположение: нужен чтобы Symfony использовал правильный класс Пользователя для получения объекта пользователя
     * @param string $class
     * @return bool
     */
    publicfunction supportsClass($class)
    {
        return$class==='Acme\\DemoBundle\\Entity\\User';
    }

    privatefunction getUserByWindowsLive($username='')
    {
        return$user=$this->doctrine->getRepository('AcmeDemoBundle:User')->findOneBy(array('windows_live'=>$username));
    }
}

Сущность пользователя

Чтобы используя Doctrine2 добавить пользователя в базу данных или проверить есть ли такой пользователь, нам нужен класс сущности. Создадим файл /src/Acme/DemoBundle/Entity/User.php со следующим содержимым:

<?php
/**
 * сущность пользователя с признаками OAuthUser
 */
namespace Intranet\AuthBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
 * @ORM\Entity(repositoryClass="Intranet\AuthBundle\Repository\UserRepository")
 * @ORM\Table(name="users", uniqueConstraints={@ORM\UniqueConstraint(name="email",columns={"email"})})
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private$id=0;

    /**
     * @ORM\Column(type="string", length=255, options={"default":""})
     */
    private$email='';

    /**
     * @ORM\Column(type="integer", options={"default":0})
     */
    private$time_created=0;

    /**
     * @ORM\Column(type="string", length=255, options={"default":""})
     */
    private$name='';

    /**
     * @ORM\Column(type="string", length=16, options={"default":""})
     */
    private$windows_live='';

    /**
     * Get id
     *
     * @return integer
     */
    publicfunction getId()
    {
        return$this->id;
    }

    /**
     * Set email
     *
     * @param string $email
     * @return User
     */
    publicfunction setEmail($email)
    {
        $this->email=(string)$email;

        return$this;
    }

    /**
     * Get email
     *
     * @return string
     */
    publicfunction getEmail()
    {
        return$this->email;
    }

    /**
     * Set time_created
     *
     * @param integer $timeCreated
     * @return User
     */
    publicfunction setTimeCreated($timeCreated)
    {
        $this->time_created=$timeCreated;

        return$this;
    }

    /**
     * Get time_created
     *
     * @return integer
     */
    publicfunction getTimeCreated()
    {
        return$this->time_created;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return User
     */
    publicfunctionsetName($name)
    {
        $this->name=(string)$name;

        return$this;
    }

    /**
     * Get name
     *
     * @return string
     */
    publicfunction getName()
    {
        return$this->name;
    }

    /**
     * Set windows_live
     *
     * @param string $windowsLive
     * @return User
     */
    publicfunction setWindowsLive($windowsLive)
    {
        $this->windows_live=(string)$windowsLive;

        return$this;
    }

    /**
     * Get windows_live
     *
     * @return string
     */
    publicfunction getWindowsLive()
    {
        return$this->windows_live;
    }

    /* ниже прописаны методы необходимые для работы oAuth-авторизации */

    /**
     * создан, т.к. этого требует интерфейс UserInterface
     * @return string - обязан возвращать уникальный ID пользователя
     */
    publicfunction getUsername()
    {
        return$this->windows_live;
    }

    /**
     * роли пользователеля, то благодаря чему Аутентифицированный пользователь приобретает статут Авторизованного
     */
    publicfunction getRoles()
    {
        returnarray('ROLE_USER','ROLE_OAUTH_USER');
    }

    /**
     * @var переменная и ниже прописанные гетер и сетер обязательны, т.к. этого требует интерфейс UserInterface
     */
    private$password;

    publicfunction setPassword($password='')
    {
        $this->password=(string)$password;
    }

    publicfunction getPassword()
    {
        return$this->password;
    }

    /**
     * создан, т.к. этого требует интерфейс UserInterface
     * @return null|string - отдает пустую строку т.к. при oAuth не используется
     */
    publicfunction getSalt()
    {
        return'';
    }

    /**
     * создан, т.к. этого требует интерфейс UserInterface
     */
    publicfunction eraseCredentials()
    {
    }
}

Класс репозитория

Ну, а раз мы упомянули в описании сущности класс репозитория пользователя, то создадим и его, все равно ведь потом пригодится. Поэтому создайте файл /src/Acme/DemoBundle/Repository/UserRepository.php со следующим содержимым:

<?php
/**
 * Класс для редактирования пользователя
 */
namespace Acme\DemoBundle\Repository;

use Acme\DemoBundle\Entity\User;

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
}

Подключение нашего провайдера (класса, который мы написали выше)

Опишу 2 примера, ведь точно не знаю, как у Вас устроено, на основе xml или  yml файла.

Настройки на основе xml файла /src/Acme/DemoBundle/Resources/config/services.xml Добавьте в свой файл то, что выделено крассным.

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <parameters>
        <parameter key="ib_user.oauth_user_provider.class">Acme\DemoBundle\Security\Provider</parameter>
    </parameters>
    <services>
        <service id="ib_user.oauth_user_provider" class="%ib_user.oauth_user_provider.class%">
          <argument type="service" id="doctrine" />
        </service>
        <service id="twig.extension.acme.demo" class="Acme\DemoBundle\Twig\Extension\DemoExtension" public="false">
            <tag name="twig.extension" />
            <argument type="service" id="twig.loader" />
        </service>

        <service id="acme.demo.listener" class="Acme\DemoBundle\EventListener\ControllerListener">
            <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
            <argument type="service" id="twig.extension.acme.demo" />
        </service>
    </services>
</container>

Настройки на основе yml файла /src/Acme/DemoBundle/Resources/config/services.yml

parameters:
    ib_user.oauth_user_provider.class: Acme\DemoBundle\Security\Provider

services:
    ib_user.oauth_user_provider:
        class: %ib_user.oauth_user_provider.class%
        arguments: [@doctrine]

Подключаем роуты

Пропишите следующее в файл /app/config/routing.yml

hwi_oauth_login:
    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
    prefix:   /login

hwi_oauth_redirect:
    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
    prefix:   /connect

windows_live_login:
    pattern: /login/check-windows_live

logout:
    path:   /logout

Настройки безопасности

Выкладываю содержимое файла /app/config/security.yml и внем выделю жирным то, что добавил:

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        chain_provider:
            chain:
                providers: [in_memory, user_db]
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
#	user_db - просто название провайдера (кроме как здесь, более нигде название не светится)
        user_db:
#          ИД провайдера (прописано в /src/Acme/DemoBundle/Resources/config/services.xml
            id: ib_user.oauth_user_provider

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern:  ^/demo/secured/login$
            security: false

        secured_area:
            pattern:    ^/demo/secured/
            form_login:
                check_path: _security_check
                login_path: _demo_login
            logout:
                path:   _demo_logout
                target: _demo
            #anonymous: ~
            #http_basic:
            #    realm: "Secured Demo Area"
#      main - просто название фаервола (я его сам выдумал)
        main:
#           ВНИМАНИЕ: т.к. в этом фаерволе НЕ указано security: false - значит проверкой доступа займется
#           выше описанный security: encoders: Symfony\Component\Security\Core\User\User: plaintext

            pattern: ^/
#           расскомментируем, чтобы начал работать oAuth-бандл HWIOAuthBundle
            anonymous: true
            logout: true
            logout:
                path:   /logout
                target: /
#           укажем название ресурса авторизации, чтобы начал работать oAuth-бандл HWIOAuthBundle
            oauth:
#                укажим владельцев ресурса
                 resource_owners:
#                     owner_windows_live - название владельца указанное в /app/config/config.yml
#                     значение данного владельца - урл по которому будут обращаться пользователи
                      owner_windows_live: "/login/check-windows_live"
#                урл, где лежит форма авторизации ( если пользователь не авторизован - отправляем его на страницу с именем роутинга _demo_login - /demo/secured/login )
                 login_path: /login
#                наверное: урл страницы, куда попадает пользователь, если не авторизовался (ввел неправильно данные или еще что-либо)
                 failure_path: /login
#                oAuth-бандлу HWIOAuthBundle нужен сервис, который позволяет загружать пользователей, основываясь на oauth-действиях пользователя
#                конечной точки. Если бы у нас была своя пользовательская служба, то она должена была бы реализовать интерфейс:
#                HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface.
                 oauth_user_provider:
#                     HWIOAuthBundle поставляется с тремя реализациями по умолчанию:
#                     OAuthUserProvider (сервисное название: hwi_oauth.user.provider) - данные не сохраняют пользователей
#                     EntityUserProvider (сервисное название: hwi_oauth.user.provider.entity) - загружает пользователей из базы данных
#                     FOSUserBundle интеграция (сервисное название: hwi_oauth.user.provider.fosub_bridge).
                      service: ib_user.oauth_user_provider

    access_control:
##        - { path: ^/demo/secured/hello/admin/, roles: ROLE_ADMIN }
        #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
        - { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/connect, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, role: ROLE_USER }

База данных

Я надеюсь Вы не забыли создать базу данных и прописать настройки подключения к ней в файле /app/config/parameters.yml

Symfony2 Microsoft Live oAuth 2.0

Что тут скажешь, наверное это все, ведь после всех проделанных Выше действий Вы можете зайти на свой сайт, он перекинет Вас на страницу /login где будет ссылка на авторизацию!

p.s. не забывайте про то, как разлогиниться https://login.live.com/oauth20_logout.srf?client_id=CLIENT_ID&redirect_uri=REDIRECT_URL


Мои проблемы

1. Нужно было в конфиге включать работу провайдера начиная от корня сайта.

2. Выпала ошибка: No oauth code in the request.

Решение: нужно в файле /app/config/config.yml указать вид информации о пользователе, запрашиваемой у сервиса windows_live для своего ресурса, например так:

scope:               wl.signin 

3. Если WindowsLive спрашивает у клиента (или у Вас при тестировании) пароль для подтверждения, например так:

Symfony2 Microsoft Live oAuth 2.0 

значит, Вы в файле /app/config/config.yml указали строгий scope

4. Вываливалась ошибка:

The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?

оказалось:

  • двух фаерволов с одинаковыми pattern быть не должно, т.к. работает только первый (вышестоящий)
  • я забыл в файле добавить для фаервола следующие параметры:
            form_login:
                check_path: _security_check
                login_path: _demo_login
            logout:
                path:   _demo_logout
                target: _demo
            pattern: ^/

5. Что-то сделал, определенно невнятную ошибку - почисти кэш, возможно ошибка пропадет (короче rm -rf app/cache/* никто не отменял :).

Источники:

p.s. тем, кто въехал во все по полной, может подключить свой сайт в качестве oAuth-сервера, благо для этого под Симфони уже написали бандл »

p.s. не по теме - понравился пример двух фаерволов:

firewalls:
        install:
            pattern:^/install/.*
            security:false
        main:
            pattern:^/(?!install/)

18.10.2013 14:37