Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
408 views
in Technique[技术] by (71.8m points)

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

I would like to provide a RESTful API secured with OAuth2 using FOSOAuthServerBundle and I'm not really sure about what I have to do.

I followed basic steps from the documentation but some things are missing and I can't find a complete example of what I need.

So, I tried to understand the best I could this example of implementation (the only one I found) but there are still things I don't understand.

First, why do we need a login page in an API? Let's suppose my client is a iPhone or Android App, I see the interest of the login page on the app, but I think the client have just to call a webservice from the API to get its token, am I wrong? So how to implement autorisation and token providing via REST endpoint?

Then, documentation tells to write this firewall :

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

And I don't know how to add an authentication process. Should I write my own one, for example following this tutorial or am I completely wrong?

Globally, can someone take the time to explain the process needed, after the five steps in the docs, to provide an OAuth2 secured RESTful API? It would be very nice...


EDIT after @Sehael answer :

I still have some questions before it is perfect...

What is represented by the "Client" here? For exemaple, should I create a client for a iPhone app, and another for an Android app? and do I have to create a new client for every instance wanting to use the API? what is the best practice in this case?

Unlike you, I don't use the OAuth process for the front website but the "classic" symfony way. Does it seem strange to you, or is it normal?

What is the usefulness of the refresh_token? How to use it?

I tried to test my new OAuth protected services. I used POSTman chrome extension, which support OAuth 1.0, does OAuth2 look like OAuth1 enough to be tested with POSTman? There is a "secret token" field that I don't know how to fill. If I can't, I'd be glad to see your (@Sehael) PHP class, as you proposed. / Edit : OK I think I found the answer for this one. I just added access_token as GET parameter with the token as value. It seems to be working. It is unfortunate that I have to do reverse engenering on bundle code to find that instead of read it in documentation.

Anyway, thanks a lot !

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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:        BBAuthBundleEntityClient
    access_token_class:  BBAuthBundleEntityAccessToken
    refresh_token_class: BBAuthBundleEntityRefreshToken
    auth_code_class:     BBAuthBundleEntityAuthCode
    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 AcmeDemoBundleCommand;

use SymfonyBundleFrameworkBundleCommandContainerAwareCommand;
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

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();
$

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...