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/)
Оцени публикацию:
  • 0,0
Оценили человек: 0

Похожие статьи:

Справочники и учебники:


Предложения и пожелания:
Ваше имя:
Ваш E-mail:
Сколько будет Οдин + Τри
Главная
X

Новые заметки:

Про что мы забываем когда делаем оценку задачи по времени

Список вопросов для собеседования разработчика по телефону

Symfony2 авторизация без Doctrine2 для чайника

Phpstorm7 LiveEdit

Жесткий хабр или не хабр, тогда кто?

Яндекс.Деньги мошенничество

Как узнать какие страницы в поиске яндекса или это секрет

Последние комменты:

Yapro CMS:

Здравствуйте, Гость | Войти | Регистрация | Карта сайта | RSS ленты | Ошибка в тексте? Выделите её мышкой и нажмите: Ctrl + Enter

youtube.com/watch?v=7hFivbgIEqk

При полном или частичном использовании материалов данного сайта, ссылка на сайт "yapro.ru" обязательна как на источник информации.
Автоматический импорт материалов и информации с сайта запрещен.
Copyrights © 2007 - 2018 YaPro.Ru

Главная » Веб-мастеру » PHP »