web-dev-qa-db-fra.com

Comment implémenter FosOAuthServerBundle pour sécuriser une API REST?

Je voudrais fournir une API RESTful sécurisée avec OAuth2 à l'aide de FOSOAuthServerBundle et je ne suis pas vraiment sûr de ce que je dois faire.

J'ai suivi les étapes de base de la documentation mais certaines choses manquent et je ne trouve pas un exemple complet de ce dont j'ai besoin.

J'ai donc essayé de comprendre du mieux que je pouvais cet exemple d'implémentation (le seul que j'ai trouvé) mais il y a encore des choses que je ne comprends pas.

Tout d'abord, pourquoi avons-nous besoin d'une page de connexion dans une API? Supposons que mon client soit un iPhone ou Android App, je vois l'intérêt de la page de connexion sur l'application, mais je pense que le client n'a qu'à appeler un webservice depuis l'API pour obtenir son token , ai-je tort? Alors, comment implémenter l'autorisation et la fourniture de jetons via le point de terminaison REST?

Ensuite, la documentation indique d'écrire ce pare-feu:

oauth_authorize:
    pattern:    ^/oauth/v2/auth
    # Add your favorite authentication process here

Et je ne sais pas comment ajouter un processus d'authentification. Dois-je écrire le mien, par exemple en suivant ce tutoriel ou je me trompe complètement?

Globalement, quelqu'un peut-il prendre le temps d'expliquer le processus nécessaire, après les cinq étapes des documents, pour fournir une API RESTful sécurisée OAuth2? Ce serait très sympa...


MODIFIER après la réponse @Sehael:

J'ai encore quelques questions avant que ce soit parfait ...

Que représente ici le "Client"? Par exemple, dois-je créer un client pour une application iPhone et un autre pour une application Android? Et dois-je créer un nouveau client pour chaque instance souhaitant utiliser l'API? meilleure pratique dans ce cas?

Contrairement à vous, je n'utilise pas le processus OAuth pour le site Web avant mais la manière symfony "classique". Cela vous semble-t-il étrange ou est-ce normal?

Quelle est l'utilité du refresh_token? Comment l'utiliser?

J'ai essayé de tester mes nouveaux services protégés OAuth. J'ai utilisé POSTman chrome extension, qui prend en charge OAuth 1.0, OAuth2 ressembler suffisamment à OAuth1 pour être testé avec POSTman? Il y a un champ "jeton secret" que je ne sais pas comment remplir. Si je ne peux pas, je serais heureux de voir votre (@Sehael) PHP class, comme vous l'avez proposé./Edit: OK je pense avoir trouvé la réponse pour celle-ci. Je viens d'ajouter access_token comme paramètre GET avec le jeton comme valeur. Cela semble fonctionner. Il est malheureux que je doive faire de la génération inverse sur le code du bundle pour le trouver au lieu de le lire dans la documentation.

Quoi qu'il en soit, merci beaucoup!

36
maphe

J'ai également constaté que la documentation peut être un peu déroutante. Mais après plusieurs heures d'essais, je l'ai compris à l'aide de ce blog (mise à jour - le blog n'existe plus, changé en Internet Archive). Dans votre cas, vous n'avez pas besoin d'une entrée de pare-feu pour ^/oauth/v2/auth Car il s'agit de la page d'autorisation. Vous devez vous rappeler ce que oAuth est capable de faire ... il est utilisé pour plus qu'un simple REST api. Mais si un REST api est ce que vous voulez protéger, vous n'en avez pas besoin. Voici un exemple de configuration de pare-feu de mon application:

firewalls:

    oauth_token:
        pattern:    ^/oauth/v2/token
        security:   false

    api_firewall:
        pattern: ^/api/.*
        fos_oauth: true
        stateless: true
        anonymous: false

    secure_area:
        pattern:    ^/
        fos_oauth: true
        form_login:
            provider: user_provider 
            check_path: /oauth/v2/auth_login_check
            login_path: /oauth/v2/auth_login
        logout:
            path:   /logout
            target: /
        anonymous: ~

access_control:
    - { path: ^/oauth/v2/auth_login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: IS_AUTHENTICATED_FULLY }

Notez que vous devez définir un fournisseur d'utilisateurs. Si vous utilisez FOSUserBundle, un fournisseur d'utilisateurs a déjà été créé pour vous. Dans mon cas, j'ai créé le mien et en ai créé un service.

Et dans mon config.yml:

fos_oauth_server:
    db_driver: orm
    client_class:        BB\AuthBundle\Entity\Client
    access_token_class:  BB\AuthBundle\Entity\AccessToken
    refresh_token_class: BB\AuthBundle\Entity\RefreshToken
    auth_code_class:     BB\AuthBundle\Entity\AuthCode
    service:
        user_provider: platform.user.provider
        options:
            supported_scopes: user

Je dois également mentionner que les tables que vous créez dans la base de données (access_token, client, auth_code, refresh_token) doivent avoir plus de champs que ce qui est indiqué dans les documents ...

Tableau des jetons d'accès: id (int), client_id (int), user_id (int), token (string), scope (string), expires_at (int )

Table client: id (int), random_id (chaîne), secret (chaîne), redirect_urls (chaîne), allowed_grant_types (chaîne)

Table de codes d'authentification: id (int), client_id (int), user_id (int)

Actualiser la table des jetons: id (int), client_id (int), user_id (int), token (string), expires_at (int), scope (string )

Ces tables stockeront les informations nécessaires pour oAuth, donc mettez à jour vos entités Doctrine afin qu'elles correspondent aux tables db comme ci-dessus.

Et puis vous avez besoin d'un moyen de générer réellement le secret et client_id, c'est donc là que la section "Création d'un client" des documents entre en jeu, bien que ce ne soit pas très utile ...

Créez un fichier dans /src/My/AuthBundle/Command/CreateClientCommand.php (Vous devrez créer le dossier Command) Ce code provient de l'article que j'ai lié à ci-dessus et montre un exemple de ce que vous pouvez mettre dans ce fichier:

<?php
# src/Acme/DemoBundle/Command/CreateClientCommand.php
namespace Acme\DemoBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CreateClientCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('acme:oauth-server:client:create')
            ->setDescription('Creates a new client')
            ->addOption(
                'redirect-uri',
                null,
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.',
                null
            )
            ->addOption(
                'grant-type',
                null,
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                'Sets allowed grant type for client. Use this option multiple times to set multiple grant types..',
                null
            )
            ->setHelp(
                <<<EOT
                    The <info>%command.name%</info>command creates a new client.

<info>php %command.full_name% [--redirect-uri=...] [--grant-type=...] name</info>

EOT
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default');
        $client = $clientManager->createClient();
        $client->setRedirectUris($input->getOption('redirect-uri'));
        $client->setAllowedGrantTypes($input->getOption('grant-type'));
        $clientManager->updateClient($client);
        $output->writeln(
            sprintf(
                'Added a new client with public id <info>%s</info>, secret <info>%s</info>',
                $client->getPublicId(),
                $client->getSecret()
            )
        );
    }
}

Ensuite, pour créer réellement le client_id et le secret, exécutez cette commande à partir de la ligne de commande (cela insérera dans la base de données les identifiants et les éléments nécessaires):

php app/console acme:oauth-server:client:create --redirect-uri="http://clinet.local/" --grant-type="password" --grant-type="refresh_token" --grant-type="client_credentials"

notez que acme:oauth-server:client:create peut être ce que vous nommez réellement votre commande dans le fichier CreateClientCommand.php avec $this->setName('acme:oauth-server:client:create').

Une fois que vous avez le client_id et le secret, vous êtes prêt à vous authentifier. Faites une demande dans votre navigateur qui ressemble à ceci:

http://example.com/oauth/v2/token?client_id=[CLIENT_ID_YOU GENERATED]&client_secret=[SECRET_YOU_GENERATED]&grant_type=password&username=[USERNAME]&password=[PASSWORD]

J'espère que cela fonctionne pour vous. Il y a certainement beaucoup à configurer, essayez simplement de le faire étape par étape.

J'ai également écrit une simple classe PHP pour appeler mon Symfony REST api en utilisant oAuth, si vous pensez que ce serait utile, faites-le moi savoir et je peux le passer) sur.

[~ # ~] mise à jour [~ # ~]

En réponse à vos autres questions:

Le "client" est décrit sur le même blog, juste un article différent. Lisez la section Clients et étendues ici, cela devrait clarifier pour vous ce qu'est un client. Comme mentionné dans l'article, vous n'avez pas besoin d'un client pour chaque utilisateur. Vous pouvez avoir un seul client pour tous vos utilisateurs si vous le souhaitez.

En fait, j'utilise également l'authentification Symfony classique pour mon site frontal, mais cela pourrait changer à l'avenir. Il est donc toujours bon de garder ces choses à l'esprit, mais je ne dirais pas qu'il est étrange de combiner les deux méthodes.

Le refresh_token est utilisé lorsque le access_token a expiré et que vous souhaitez demander un nouveau access_token sans renvoyer les informations d'identification de l'utilisateur. à la place, vous envoyez le jeton d'actualisation et obtenez un nouveau access_token. Ce n'est pas vraiment nécessaire pour une API REST car une seule demande ne prendra probablement pas assez de temps pour que access_token expire.

oAuth1 et oAuth2 sont très différents, donc je suppose que la méthode que vous utilisez ne fonctionnerait pas, mais je n'ai jamais essayé avec ça. Mais juste pour les tests, vous pouvez faire une requête GET normale ou POST, tant que vous passez le access_token=[ACCESS_TOKEN] Dans la chaîne de requête GET (pour tous les types de requêtes, en fait) .

Mais de toute façon, voici ma classe. J'ai utilisé un fichier de configuration pour stocker certaines variables, et je n'ai pas implémenté la possibilité de SUPPRIMER, mais ce n'est pas trop difficile.

class RestRequest{
    private $token_url;
    private $access_token;
    private $refresh_token;
    private $client_id;
    private $client_secret;

    public function __construct(){
        include 'config.php';
        $this->client_id = $conf['client_id'];
        $this->client_secret = $conf['client_secret']; 
        $this->token_url = $conf['token_url'];

        $params = array(
            'client_id'=>$this->client_id,
            'client_secret'=>$this->client_secret,
            'username'=>$conf['rest_user'],
            'password'=>$conf['rest_pass'],
            'grant_type'=>'password'
        );

        $result = $this->call($this->token_url, 'GET', $params);
        $this->access_token = $result->access_token;
        $this->refresh_token = $result->refresh_token;
    }

    public function getToken(){
        return $this->access_token;
    }

    public function refreshToken(){
        $params = array(
            'client_id'=>$this->client_id,
            'client_secret'=>$this->client_secret,
            'refresh_token'=>$this->refresh_token,
            'grant_type'=>'refresh_token'
        );

        $result = $this->call($this->token_url, "GET", $params);

        $this->access_token = $result->access_token;
        $this->refresh_token = $result->refresh_token;

        return $this->access_token;
    }

    public function call($url, $method, $getParams = array(), $postParams = array()){
        ob_start();
        $curl_request = curl_init();

        curl_setopt($curl_request, CURLOPT_HEADER, 0); // don't include the header info in the output
        curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1); // don't display the output on the screen
        $url = $url."?".http_build_query($getParams);
        switch(strtoupper($method)){
            case "POST": // Set the request options for POST requests (create)
                curl_setopt($curl_request, CURLOPT_URL, $url); // request URL
                curl_setopt($curl_request, CURLOPT_POST, 1); // set request type to POST
                curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams)); // set request params
                break;
            case "GET": // Set the request options for GET requests (read)
                curl_setopt($curl_request, CURLOPT_URL, $url); // request URL and params
                break;
            case "PUT": // Set the request options for PUT requests (update)
                curl_setopt($curl_request, CURLOPT_URL, $url); // request URL
                curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "PUT"); // set request type
                curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams)); // set request params
                break;
            case "DELETE":

                break;
            default:
                curl_setopt($curl_request, CURLOPT_URL, $url);
                break;
        }

        $result = curl_exec($curl_request); // execute the request
        if($result === false){
            $result = curl_error($curl_request);
        }
        curl_close($curl_request);
        ob_end_flush();

        return json_decode($result);
    }
}

Et puis pour utiliser la classe, juste:

$request = new RestRequest();
$insertUrl = "http://example.com/api/users";
$postParams = array(
    "username"=>"test",
    "is_active"=>'false',
    "other"=>"3g12g53g5gg4g246542g542g4"
);
$getParams = array("access_token"=>$request->getToken());
$response = $request->call($insertUrl, "POST", $getParams, $postParams);
53
Sehael