OAuth2 w Symfony2 krok po kroku

Poprzednio omówiłem ideę autoryzacji przez OAuth2. Dziś czas na praktykę. Zbudujemy od zera serwer autoryzacji w Symfony2. Opiszemy proces krok po kroku aby nawet początkujący w Symfony potrafili uruchomić nasz serwer.

Do stworzenia serwera wykorzystamy gotowy bundle FOSAuthServerBundle.

Krok 1 – tworzenie aplikacji.

Na początek utwórzmy naszą aplikację. Nazwiemy ją OAuth2_Example:

$ symfony new OAuth2_Example

Ustawmy od razu dostęp do bazy w pliku app/config/parameters.yml. Następnie stwórzmy bazę:

$ cd OAuth2_Example
$ php app/console doctrine:database:create
Created database for connection named `oauth2_example`

Instalujemy bundla. W pliku composer.json dodajemy wpis:

"require": {
   "friendsofsymfony/oauth-server-bundle": "dev-master"
}

Po czym instalujemy:

$ composer update

Krok 2 – użytkownicy

Aby cokolwiek autoryzować musimy najpierw stworzyć konta użytkowników. Utwórzmy sobie bundla, który będzie zajmował się użytkownikami:

$ php app/console generate:bundle --namespace=Example/UserBundle --format=yml

Teraz musimy utworzyć tabelę z użytkownikami. Bazując na opisie w Symfony Cookbook tworzyy plik src/Example/UserBundle/Entity/User.php:

<?php 
// src/Example/UserBundle/Entity/User.php
namespace Example\UserBundle\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
/**
 * Example\UserBundle\Entity\User
 * 
 * @ORM\Entity 
 */
class User implements UserInterface, \Serializable {
   /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO") 
    */
   private $id;

   /**
    * @ORM\Column(type="string", length=25, unique=true)
    */
   private $username;

   /** 
    * @ORM\Column(type="string", length=64)
    */
   private $password;

   /** 
    * @ORM\Column(type="string", length=60, unique=true)
    */
   private $email;

   /**
    * @ORM\Column(name="is_active", type="boolean")
    */
   private $isActive;

   public function __construct() 
   {
      $this->isActive = true;
   }

   public function getUsername()
   {
      return $this->username;
   }

   public function getSalt()
   {
      return null;
   }

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

   public function getRoles()
   {
      return array('ROLE_USER');
   }

   public function eraseCredentials()
   {
   }

   public function serialize()
   {
      return serialize([$this->id,$this->username,$this->password]);
   }

   public function unserialize($serialized)
   {
      list ($this->id,$this->username,$this->password,) = unserialize($serialized);
   }
}

Brakujące settery i gettery dodajemy komendą:

$ php app/console doctrine:generate:entities Example/UserBundle/Entity/User

Generujemy tabelę w bazie:

$ php app/console doctrine:schema:update --force

Teraz musimy utworzyć jakiegoś użytkownika. Najprościej skorzystać z fixtures dodając taki kod:

<?php 
//src/Example/UserBundle/DataFixtures/ORM/LoadUsers.php
namespace Example\UserBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Example\UserBundle\Entity\User;

class LoadUsers implements FixtureInterface, ContainerAwareInterface {
   private $container;
   public function setContainer(ContainerInterface $container = null) {
      $this->container = $container;
   }

   public function load(ObjectManager $manager) {
      $user = new User();

      $password = 'pomidor';
      $encoder = $this->container->get('security.password_encoder');
      $encoded = $encoder->encodePassword($user, $password);

      $user->setUsername('admin');
      $user->setPassword($encoded);
      $user->setEmail('admin@example.com');

      $manager->persist($user);
      $manager->flush();

   }
}

Teraz tylko aktualizujemy dane:

$ php app/console doctrine:fixtures:load

Pozostaje ustawienie zabezpieczeń w pliku security.yml. Pełna treść będzie wyglądać mniej-więcej tak:

security:
   encoders:
      Example\UserBundle\Entity\User:
         algorithm: bcrypt
         cost: 12

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

      providers:
         administrators:
            entity: { class: ExampleUserBundle:User, property: username }

      firewalls:
         admin_area:
            pattern:    ^/admin
            http_basic: ~

      access_control:
         - { path: ^/admin, roles: ROLE_ADMIN }

Możemy na próbę zrobić prosty kontroler w ścieżce „/admin”. Przeglądarka powinna poprosić nas o autoryzację.

Gdy sprawdzimy, że użytkownicy działają możemy przejść do właściwej części:

Krok 3 – Serwer OAuth2

Na początek stwórzmy sobie bundla odpowiedzialnego za autoryzację. Nazwiemy go Example/ApiBundle:

$ php app/console generate:bundle --namespace=Example/ApiBundle --format=yml

Musimy teraz zdefiniować cztery encje. Będą one w bazie przechowywać nasze tokeny, klientów i kody autoryzacji. Kolejno dodamy:

  • Client – dane kont klientów korzystających z OAuth2
  • AccessToken i RefreshToken – tokeny przyznawane klientom
  • AuthCode – kody autoryzacji

Nie będę wklejał pełnego kodu gdyż jest on w dokumentacji FOSOAuthServerBundle. Kopiujemy kod czterech klas Doctrine do odpowiednich plików.

Uwaga na odwołanie do użytkowników. Wszędzie w kodzie, gdzie mamy wskazanie na encję „Your\Own\Entity\User” zamieniamy je na naszą „Example\UserBundle\Entity\User”.

Generujemy tabele w bazie:

$ php app/console doctrine:schema:update --force

Podłączamy nasze encje do konfiguracji. Ponieważ FOSOAuthServerBundle obsługuje różne bazy danych (Doctrine, Propel, ODM), musimy wskazać z jakiej korzystamy. W pliku config.yml dodajemy:

# app/config/config.yml
fos_oauth_server:
    db_driver:           orm
    client_class:        Example\ApiBundle\Entity\Client
    access_token_class:  Example\ApiBundle\Entity\AccessToken
    refresh_token_class: Example\ApiBundle\Entity\RefreshToken
    auth_code_class:     Example\ApiBundle\Entity\AuthCode

Aktualizujemy konfigurację security. Dodajemy takie definicje do security.yml:

# app/config/security.yml
security:
    firewalls:
        oauth_token:
            pattern:    ^/oauth/v2/token
            security:   false

        oauth_authorize:
            pattern:    ^/oauth/v2/auth
            http_basic: ~

        api:
            pattern:    ^/api
            fos_oauth:  true
            stateless:  true

    access_control:
        - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }

Do ustawienia został nam już tylko routing:

# app/config/routing.yml
fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

fos_oauth_server_authorize:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"

Na koniec dodajemy właściwy bundle do konfiguracji:

// app/appKernel.php
...
new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
...

Krok 4 – Uruchamiamy autoryzację

Jeśli wszystko poszło dobrze będziemy mieć dwie ważne ścieżki:

  • /oauth/v2/auth – miejsce, gdzie przekierujemy z dowolnej aplikacji aby użytkownik zalogował się i zezwolił na dostęp. Może tu od razu otrzymać token przy pośredniej (implict) autoryzacji
  • /oauth/v2/token – miejsce, gdzie aplikacja otrzyma tokeny

Jeśli teraz wejdziemy przeglądarką pod adres /oauth/v2/auth otrzymamy… błąd 404 z informacją „Client not found”. Oczywiście – serwer nie zna żadnego klienta a na dodatek przeglądarka nie przedstawiła się żadnym id klienta.

Naprawmy to dodając klienta. Musimy gdzieś (np. w jakimś kontrolerze) wywołać kod:

/**
 * @Route("/addClient", name="_adduser")
 */
public function addclientAction()
{
   $clientManager = $this->get('fos_oauth_server.client_manager.default');
   $client = $clientManager->createClient();
   $client->setRedirectUris(array('http://adam.wroclaw.pl'));
   $client->setAllowedGrantTypes(array('token', 'authorization_code'));
   $clientManager->updateClient($client);

   $output = sprintf("Added client with id: %s secret: %s",$client->getPublicId(),$client->getSecret());
   return new Response($output);
}

W odpowiedzi dostaniemy odpowiedź w stylu:

Added client with id: 1_586bzkmpn8wswk4gwcg0888gscgwgo0w0s0wgcok0g04o08wk0

Pierwszy kod to nasz publiczny identyfikator klienta. Spróbujmy gdzieś na boku utworzyć stronę, która będzie się autoryzować przez nasz serwer. Dla uproszczenia skorzystamy z pośredniej (implict) autoryzacji.

W tym celu musimy przygotować URL składający się z:

  • Adresu naszego serwera wraz ze ścieżką autoryzacji. Niech to będzie np. http://nasz-serwer.pl/oauth/v2/auth
  • Identyfikatora klienta, który przed chwilą dostaliśmy
  • Adresu powrotnego, czyli http://adam.wroclaw.pl. To ten sam adres, który podaliśmy tworząc klienta
  • Rodzaju autoryzacji. W parametrze „type” podajemy „token”

Nasz adres będzie wyglądał tak:

http://nasz-serwer.pl/oauth/v2/auth?client_id=CLIENT_ID&redirect_uri=http%3A//adam.wroclaw.pl&response_type=token

Tworzymy teraz plik html, w którym w najprostszej wersji możemy wstawić link do autoryzacji:

<a href="http://nasz-serwer.pl/oauth/v2/auth?client_id=CLIENT_ID&amp;redirect_uri=http%3A//adam.wroclaw.pl&amp;response_type=token">Autoryzuj</a>

Nasz serwer poprosi nas o zalogowanie a potem o zgodę na skorzystanie z zasobów. Jeśli zrobimy wszystko poprawnie, zostaniemy przekierowani na adres typu:

http://adam.wroclaw.pl/#access_token=MTAxOWJlYmQ1NDMzNzA2YjA5MDMzOTBjNjY0YTg0ZDY5NWRjYWY0MTRjZjkwOTBmY2Q4MDhkYmMyMjA3ZTVhYQ&expires_in=3600&token_type=bearer

Widać, że po hashu dostaliśmy nasz token. W bazie również znajdziemy go w tabeli access_token. Jeśli wszystko zadziałało, możemy cieszyć sie poprawnie ustawionym serwerem autoryzacji.

Co dalej

Warto przeczytać dokumentację do FOSOAuthServerBundle. Można też znaleźć bardzo dobry artykuł o OAuth2 w Symfony.

Do wyboru mamy wszystkie rodzaje autoryzacji. ożemy tworzyć również własne typy grantów.

W produkcyjnej aplikacji zechcemy na pewno zmienić ekran logowania i akceptacji. Możemy też potrzebować zakresów aby nie dawać dostępu do całej aplikacji. Wszystko to jest opisane w w/w artykułach.

Tworząc serwis REST musimy też zadbać o tokeny. Nasz właśnie zbudowany serwer autoryzacji musi w jakiś sposób przesłać tokeny do serwisu RESTowego jeśli nie są one na tym samym serwerze.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *