如何实施 FosOAuthServerBundle 以保护 REST API? [英] How to implement FosOAuthServerBundle to secure a REST API?

查看:50
本文介绍了如何实施 FosOAuthServerBundle 以保护 REST API?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想提供一个使用 FOSOAuthServerBundle 的 OAuth2 保护的 RESTful API,但我不确定我必须做什么.

我遵循了文档中的基本步骤,但是有些东西丢失了,我找不到我需要的完整示例.

所以,我试图尽可能地了解 这个实现的例子(我发现的唯一一个)但仍有一些我不明白的地方.

首先,为什么我们需要 API 中的登录页面?假设我的客户端是 iPhone 或 Android 应用程序,我在应用程序上看到登录页面的兴趣,但我认为客户端只需从 API 调用网络服务即可获取其令牌,我错了吗?那么如何通过 REST 端点实现自动化和令牌提供?

然后,文档告诉编写这个防火墙:

oauth_authorize:模式:^/oauth/v2/auth# 在此处添加您最喜欢的身份验证流程

而且我不知道如何添加身份验证过程.我应该自己写一个吗,例如按照 本教程 还是我完全错了?

在全球范围内,有人可以花时间解释在文档中的五个步骤之后提供 OAuth2 安全 RESTful API 所需的过程吗?那就太好了……

<小时>

在@Sehael 回答后

在它完美之前我还有一些问题......

这里的客户端"代表什么?例如,我应该为 iPhone 应用程序创建一个客户端,为 Android 应用程序创建另一个客户端吗?我是否必须为每个想要使用 API 的实例创建一个新客户端?在这种情况下,最佳做法是什么?

与您不同,我不使用前端网站的 OAuth 流程,而是使用经典"的 symfony 方式.你觉得奇怪,还是正常的?

refresh_token 有什么用?如何使用?

我尝试测试我的受 OAuth 保护的新服务.我使用了支持 OAuth 1.0 的 POSTman chrome 扩展,OAuth2 看起来像 OAuth1 是否足以用 POSTman 进行测试?有一个秘密令牌"字段,我不知道如何填写.如果我不能,我很高兴看到你提出的 (@Sehael) PHP 课程./好的,我想我找到了这个答案.我刚刚添加了 access_token 作为 GET 参数,并将令牌作为值.它似乎正在工作.不幸的是,我必须对捆绑代码进行反向生成才能找到它,而不是在文档中阅读它.

无论如何,非常感谢!

解决方案

我还发现文档可能有点混乱.但是经过几个小时的尝试,我在 此博客(更新 - 该博客不再存在,已更改到互联网档案).在您的情况下,您不需要 ^/oauth/v2/auth 的防火墙条目,因为这是用于授权页面.您需要记住 oAuth 能够做什么……它不仅仅用于 REST api.但是,如果 REST api 是您想要保护的东西,那么您就不需要它.这是我的应用程序中的示例防火墙配置:

防火墙:oauth_token:模式:^/oauth/v2/token安全:假api_防火墙:模式:^/api/.*fos_oauth: 真无状态:真匿名:假安全区域:模式:^/fos_oauth: 真表单登录:提供者:user_provider检查路径:/oauth/v2/auth_login_check登录路径:/oauth/v2/auth_login登出:路径:/退出目标:/匿名:~访问控制:- { 路径:^/oauth/v2/auth_login$,角色:IS_AUTHENTICATED_ANONYMOUSLY }- { 路径:^/,角色:IS_AUTHENTICATED_FULLY }

请注意,您需要定义一个用户提供程序.如果您使用 FOSUserBundle,则已经为您创建了一个用户提供程序.就我而言,我自己制作并从中创建了一项服务.

在我的 config.yml 中:

fos_oauth_server:db_driver: orm客户端类:BB\AuthBundle\Entity\Clientaccess_token_class: BB\AuthBundle\Entity\AccessTokenrefresh_token_class: BB\AuthBundle\Entity\RefreshTokenauth_code_class: BB\AuthBundle\Entity\AuthCode服务:user_provider:platform.user.provider选项:supported_scopes: 用户

我还应该提到,您在数据库中创建的表(access_token、client、auth_code、refresh_token)需要具有比文档中显示的更多的字段......

访问令牌表: id(int), client_id(int), user_id(int), token(string), scope(string), expires_at(int)

客户端表: id(int)、random_id(string)、secret(string)、redirect_urls(string)、 allowed_grant_types(string)

验证码表: id(int), client_id(int), user_id(int)

刷新令牌表: id(int), client_id(int), user_id(int), token(string), expires_at(int), scope(string)

这些表将存储 oAuth 所需的信息,因此请更新您的 Doctrine 实体,使其与上述 db 表相匹配.

然后您需要一种方法来实际生成密钥和 client_id,这就是文档的创建客户端"部分的用武之地,尽管它不是很有帮助...

/src/My/AuthBundle/Command/CreateClientCommand.php 创建一个文件(你需要创建文件夹 Command) 此代码来自文章 I链接到上面,并显示了可以放入此文件的示例:

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(冲刺('添加了一个新客户端,其公共 ID %s,秘密 %s',$client->getPublicId(),$client->getSecret()));}}

然后要实际创建 client_id 和 secret,从命令行执行此命令(这会将必要的 id 和内容插入到数据库中):

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

请注意,acme:oauth-server:client:create 可以是您在 CreateClientCommand.php 文件中使用 $this- 实际命名的任何命令>setName('acme:oauth-server:client:create').

一旦您获得了 client_id 和机密,您就可以进行身份​​验证了.在浏览器中发出如下请求:

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

希望它对你有用.肯定有很多配置,只需尝试一步一步.

我还编写了一个简单的 PHP 类来使用 oAuth 调用我的 Symfony REST api,如果您认为这有用,请告诉我,我可以传递它.

更新

回答您的进一步问题:

客户端"在同一个博客中描述,只是不同的文章.阅读 客户和范围 部分在这里,它应该向您阐明什么是客户.就像文章中提到的那样,您不需要为每个用户都配备一个客户端.如果需要,您可以为所有用户使用一个客户端.

实际上,我的前端站点也使用经典的 Symfony 身份验证,但将来可能会发生变化.所以把这些事情记在心里总是好的,但我不会说将这两种方法结合起来很奇怪.

refresh_token 在 access_token 已过期并且您想请求新的 access_token 而不重新发送用户凭据时使用.相反,您发送刷新令牌并获得新的 access_token.这对于 REST API 来说并不是真正必要的,因为单个请求可能不会花费足够长的时间来使 access_token 过期.

oAuth1 和 oAuth2 非常不同,所以我假设您使用的方法不起作用,但我从未尝试过.但只是为了测试,你可以做一个普通的 GET 或 POST 请求,只要你在 GET 查询字符串中传递access_token=[ACCESS_TOKEN](实际上对于所有类型的请求).>

但无论如何,这是我的课.我使用了一个配置文件来存储一些变量,我没有实现删除的能力,但这并不难.

class RestRequest{私人 $token_url;私人 $access_token;私人 $refresh_token;私人 $client_id;私人 $client_secret;公共函数 __construct(){包括'config.php';$this->client_id = $conf['client_id'];$this->client_secret = $conf['client_secret'];$this->token_url = $conf['token_url'];$params = 数组('client_id'=>$this->client_id,'client_secret'=>$this->client_secret,'用户名'=>$conf['rest_user'],'密码'=>$conf['rest_pass'],'grant_type'='密码');$result = $this->call($this->token_url, 'GET', $params);$this->access_token = $result->access_token;$this->refresh_token = $result->refresh_token;}公共函数 getToken(){返回 $this->access_token;}公共函数 refreshToken(){$params = 数组('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;返回 $this->access_token;}公共函数调用($url, $method, $getParams = array(), $postParams = array()){ob_start();$curl_request = curl_init();curl_setopt($curl_request, CURLOPT_HEADER, 0);//不要在输出中包含标题信息curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1);//不要在屏幕上显示输出$url = $url."?".http_build_query($getParams);开关(strtoupper($方法)){case "POST"://设置 POST 请求的请求选项(创建)curl_setopt($curl_request, CURLOPT_URL, $url);//请求地址curl_setopt($curl_request, CURLOPT_POST, 1);//设置请求类型为POSTcurl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams));//设置请求参数休息;case "GET"://设置 GET 请求的请求选项(读取)curl_setopt($curl_request, CURLOPT_URL, $url);//请求 URL 和参数休息;case "PUT"://设置 PUT 请求的请求选项(更新)curl_setopt($curl_request, CURLOPT_URL, $url);//请求地址curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "PUT");//设置请求类型curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams));//设置请求参数休息;案例删除":休息;默认:curl_setopt($curl_request, CURLOPT_URL, $url);休息;}$result = curl_exec($curl_request);//执行请求if($result === false){$result = curl_error($curl_request);}curl_close($curl_request);ob_end_flush();返回 json_decode($result);}}

然后要使用该类,只需:

$request = new RestRequest();$insertUrl = "http://example.com/api/users";$postParams = 数组(用户名"=>测试","is_active"='false',其他"=>3g12g53g5gg4g246542g542g4");$getParams = array("access_token"=>$request->getToken());$response = $request->call($insertUrl, "POST", $getParams, $postParams);

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 !

解决方案

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

这篇关于如何实施 FosOAuthServerBundle 以保护 REST API?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆