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.