How to implement FosOAuthServerBundle to secure a REST API?

拜拜、爱过 提交于 2019-11-28 03:02:23
Sehael

I have also found that the documentation can be a bit confusing. But after many hours of trying, I figured it out with the help of this blog (update - the blog no longer exists, changed to Internet Archive). In your case, you don't need a firewall entry for ^/oauth/v2/auth because this is for the authorization page. You need to remember what oAuth is able to do... it is used for more than just a REST api. But if a REST api is what you want to protect, you don't need it. here is an example firewall config from my app:

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 }

Notice that you need to define a user provider. If you use FOSUserBundle, there is a user provider already created for you. In my case, I made my own and created a service out of it.

And in my 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

I should also mention that the tables that you create in the database (access_token, client, auth_code, refresh_token) are required to have more fields than what is shown in the docs...

Access Token Table: id(int), client_id(int), user_id(int), token(string), scope(string), expires_at(int)

Client Table: id(int), random_id(string), secret(string), redirect_urls(string), allowed_grant_types(string)

Auth Code Table: id(int), client_id(int), user_id(int)

Refresh Token Table: id(int), client_id(int), user_id(int), token(string), expires_at(int), scope(string)

These tables will store the info needed for oAuth, so update your Doctrine entities so they match the db tables like above.

And then you need a way to actually generate the secret and client_id, so that's where the "Creating a Client" section of the docs comes in, although it isn't very helpful...

Create a file at /src/My/AuthBundle/Command/CreateClientCommand.php (you will need to create the folder Command) This code is from the article I linked to above, and shows an example of what you can put into this file:

<?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()
            )
        );
    }
}

Then to actually create the client_id and secret, execute this command from the command line (this will insert into the database the necessary ids and stuff):

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

notice that the acme:oauth-server:client:create can be whatever you actually name your command in the CreateClientCommand.php file with $this->setName('acme:oauth-server:client:create').

Once you have the client_id and secret, you are ready to authenticate. Make a request in your browser that is something like this:

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

Hopefully it works for you. There is definitly alot to configure, just try to take it one step at a time.

I also wrote a simple PHP class to call my Symfony REST api using oAuth, if you think that would be useful, let me know and I can pass it on.

UPDATE

In response to your further questions:

The "client" is described on the same blog, just a different article. Read the Clients and Scopes section here, it should clarify for you what a client is. Like mentioned in the article, you don't need a client for every user. You could have a single client for all your users if you want.

I actually am also using the classic Symfony authentication for my frontend site, but that may change in the future. So it's always good to keep these things in the back of your mind, but I wouldn't say that it is strange to combine the two methods.

The refresh_token is used when the access_token has expired and you want to request a new access_token without resending the user credentials. instead, you send the refresh token and get a new access_token. This isn't really necessary for a REST API because a single request probably won't take long enough for the access_token to expire.

oAuth1 and oAuth2 are very different, so I would assume that the method you use wouldn't work, but I've never tried with that. But just for testing, you can make a normal GET or POST request, as long as you pass the access_token=[ACCESS_TOKEN] in the GET query string (for all types of requests, actually).

But anyways, here is my class. I used a config file to store some variables, and I didn't implement the ability to DELETE, but that isn't too hard.

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);
    }
}

And then to use the class, just:

$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);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!