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 email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *