How to implement FosOAuthServerBundle to secure a REST API? How to implement FosOAuthServerBundle to secure a REST API? symfony symfony

How to implement FosOAuthServerBundle to secure a REST API?


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.phpnamespace 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);