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&redirect_uri=http%3A//adam.wroclaw.pl&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.