FriendsOfSymfony / FOSRestBundle

This Bundle provides various tools to rapidly develop RESTful API's with Symfony
http://symfony.com/doc/master/bundles/FOSRestBundle/index.html
MIT License
2.79k stars 703 forks source link

FOSRestBundle and WSSE Api authentication and logged user #461

Closed lukeman83 closed 11 years ago

lukeman83 commented 11 years ago

Hi, I'm developing the RestApi for native mobile apps. I integrated WSSE authentication and FOSRestBundle. Now, if I log user (by browser) and after I use the api that's all right! But I need to use api without to log user. Ii is possible to do these steps?

-authentication by wsse -give user by wsse header in controller

lsmith77 commented 11 years ago

Can you elaborate a bit more what the issue is?

lukeman83 commented 11 years ago

I would to use Restful Api from native mobile apps. (without browser and cookie) I thought to authenticate users by wsse and give logged user in restcontroller. Now I have two problems: 1)If I'm not logged the API don't respond. 2)Which is the way to get logged user in RestController using wsse authentication?

Baachi commented 11 years ago

@lukeman83 It is possible. You need to create your own security provider which authenticate the user over WSSE. Then you must configure the firewall to use you own security provider. Maybe one of this bundles can help you.

lsmith77 commented 11 years ago

would be great if someone could donate a doc chapter on this.

lukeman83 commented 11 years ago

I used a FOSRestBundleByExample https://github.com/sdiaz/FOSRestBundleByExample/blob/master/app/Resources/doc/index.md to integrate FOSRestBundle and MopaWSSEAuthenticationBundle. My own security provider is done! But I want to retrieve the logged user in a controller and I want to use the Api without classic login but with wsse authentication only.

Baachi commented 11 years ago

Retrieve the current user To retrieve the current user then you must inject the security.context service in your controller. Now you can do this:

$user = $context->getToken()->getUser();

Use WSSE only Make sure your firewall is in the right order. The first firewall wich matched the request uri will be used (WSSE, Login, ...). If you want to disable the form login completly change the pattern option in security.yml.

lukeman83 commented 11 years ago

Is this the right order?

firewalls:
        wsse_secured:
            pattern:   ^/api/.*
            stateless:    true
            wsse:
                nonce_dir: null
                lifetime: 5184000
                provider: fos_userbundle
            anonymous:    true
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: form.csrf_provider
                check_path: fos_user_security_check
                login_path: fos_user_security_login
                default_target_path: homepage
            fos_facebook:
                app_url: %facebookAppUrl%
                server_url: %facebookServerUrl%
                login_path: fos_user_security_login
                check_path: _security_check
                default_target_path: homepage
                provider: my_fos_facebook_provider
            logout:
                handlers: ["fos_facebook.logout_handler"]
            anonymous:    true
            switch_user: true

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

        oauth_authorize:
            pattern: ^/oauth/v2/auth 
            form_login:
                provider: fos_userbundle
                check_path: /oauth/v2/auth_login_check
                login_path: /oauth/v2/auth_login
            anonymous: true
            pattern:    ^/oauth/v2/auth

        api:
            pattern:    ^/api
            fos_oauth:  true
            stateless:  true
stof commented 11 years ago

@lukeman83 no it is not. the firewall pattern: ^/ should be the last one as it is a catch-all (any firewall placed after it can never been reached) However, wsse_secured and api firewalls should be combined together as they have the same pattern (you cannot trigger 2 firewalls for the same url so api can never be reached to authenticate on the API using OAuth). And oauth_authorize may not be necessary as authenticating for this url can be done by the main firewall (which is already the case currently btw because of the order)

lukeman83 commented 11 years ago

It is ok now?

firewalls:
        wsse_secured:
            pattern:   ^/api/.*
            stateless:    true
            wsse:
                nonce_dir: null
                lifetime: 5184000
                provider: fos_userbundle
            anonymous:    true

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

        oauth_authorize:
            pattern: ^/oauth/v2/auth 
            form_login:
                provider: fos_userbundle
            anonymous: true

        api:
            pattern:    ^/graph
            fos_oauth:  true
            stateless:  true

        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: form.csrf_provider
                check_path: fos_user_security_check
                login_path: fos_user_security_login
                default_target_path: homepage
            fos_facebook:
                app_url: %facebookAppUrl%
                server_url: %facebookServerUrl%
                login_path: fos_user_security_login
                check_path: _security_check
                default_target_path: homepage
                provider: my_fos_facebook_provider
            logout:
                handlers: ["fos_facebook.logout_handler"]
            anonymous:    true
            switch_user: true
lukeman83 commented 11 years ago

Or I have to combine wsse_secured and api together?

Baachi commented 11 years ago

@lukeman83 You changed the pattern from the api firewall in your second comment. What is now the right pattern, /graph or /api?

lukeman83 commented 11 years ago

I have not use oauth pattern yet. I think I can you whatever pattern I want in the future.

lukeman83 commented 11 years ago

At the moment the RESPONSE is: 403 Forbidden

And this is my token creator controller:

/**
     * WSSE Token generation
     *
     * @return FOSView
     * @throws AccessDeniedException
     * @ApiDoc()
     */
    public function postTokenCreateAction()
    {

        $view = FOSView::create();
        $request = $this->getRequest();

        $username = $request->get('_username');
        $password = $request->get('_password');

        //$csrfToken = $this->container->get('form.csrf_provider')->generateCsrfToken('authenticate');
        //$data = array('csrf_token' => $csrfToken,);

        $um = $this->get('fos_user.user_manager');
        $user = $um->findUserByUsernameOrEmail($username);

        if (!$user instanceof User) {
            throw new AccessDeniedException("Wrong user");
        }

        $created = date('c');
        $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
        $nonceHigh = base64_encode($nonce);
        $passwordDigest = base64_encode(sha1($nonce . $created . $password . "{".$user->getSalt()."}", true));
        $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
        $view->setHeader("Authorization", 'WSSE profile="UsernameToken"');
        $view->setHeader("X-WSSE", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"");
        $data = array('WSSE' => $header);
        $view->setStatusCode(200)->setData($data);
        return $view;
    }
Baachi commented 11 years ago

Is your app online or opensource?

lukeman83 commented 11 years ago

No it isn't. Maybe I need to inject the security.context service in my controller., Which is the best way to do it? @stof what do you think about?

lukeman83 commented 11 years ago

By the way...At the moment I have two problems: -I need the right way to inject the security.context service in above postTokenCreateAction() -When I try to use the api I receive 403 Forbidden Response

lsmith77 commented 11 years ago

injecting a service into an action is nothing doorsill with this bundle. either you use the base controller or some how else inject the container. alternatively you define your controller as a service and explicitly do the injection

lukeman83 commented 11 years ago

Can you show me an example please?

lsmith77 commented 11 years ago

this is basic Symfony2 stuff and covered well in the official docs.

lukeman83 commented 11 years ago

Ok, I will read the official docs. And what about 403 Forbidden Response?

lukeman83 commented 11 years ago

Is this the right way to define my controller as a service and explicitly do the injection?

    wsse_security:
        class: project\MainBundle\Controller\Api\projectApiSecurityController
        arguments: ["@security.context"]
<?php 

/**
 * This file is part of the FOSRestByExample package.
 *
 * (c) Santiago Diaz <santiago.diaz@me.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 */

namespace project\MainBundle\Controller\Api;

use project\UserBundle\Entity\User;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use FOS\RestBundle\Controller\Annotations\Prefix;
use FOS\RestBundle\Controller\Annotations\NamePrefix;
use FOS\RestBundle\View\RouteRedirectView;
use FOS\RestBundle\View\View AS FOSView;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\SecurityContextInterface;

/**
 * Controller that provides Restfuls security functions.
 *
 * @Prefix("/security")
 * @NamePrefix("project_securityrest_")
 * @author Santiago Diaz <santiago.diaz@me.com>
 */
class projectApiSecurityController extends Controller
{
    protected $securityContext;

    public function __construct(SecurityContextInterface $securityContext)
    {
        $this->securityContext = $securityContext;
    }
    /**
     * WSSE Token generation
     *
     * @return FOSView
     * @throws AccessDeniedException
     * @ApiDoc()
     */
    public function postTokenCreateAction()
    {

        $view = FOSView::create();
        $request = $this->getRequest();

        $username = $request->get('_username');
        $password = $request->get('_password');

        //$csrfToken = $this->container->get('form.csrf_provider')->generateCsrfToken('authenticate');
        //$data = array('csrf_token' => $csrfToken,);

        $um = $this->get('fos_user.user_manager');
        $user = $um->findUserByUsernameOrEmail($username);

        if (!$user instanceof User) {
            throw new AccessDeniedException("Wrong user");
        }

        $created = date('c');
        $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
        $nonceHigh = base64_encode($nonce);
        $passwordDigest = base64_encode(sha1($nonce . $created . $password . "{".$user->getSalt()."}", true));
        $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
        $view->setHeader("Authorization", 'WSSE profile="UsernameToken"');
        $view->setHeader("X-WSSE", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"");
        $data = array('WSSE' => $header);
        $view->setStatusCode(200)->setData($data);
        return $view;
    }

  /**
     * WSSE Token Remove
     *
     * @return FOSView
     * @ApiDoc()
     */
    public function getTokenDestroyAction()
    {
        $view = FOSView::create();
        $security = $this->get('security.context');
        $token = new AnonymousToken(null, new User());
        $security->setToken($token);
        $this->get('session')->invalidate();
        $view->setStatusCode(200)->setData('Logout successful');
        return $view;
    }
}
Baachi commented 11 years ago

Yes this is one method to inject the security.context.

lukeman83 commented 11 years ago

It doesn't work:

Catchable Fatal Error: Argument 1 passed to project\MainBundle\Controller\Api\projectApiSecurityController::__construct() must be an instance of project\MainBundle\Controller\Api\Symfony\Component\Security\Core\SecurityContextInterface, none given, called in C:\xampp\htdocs\project\app\cache\dev\jms_diextra\controller_injectors\projectMainBundleControllerApiprojectApiSecurityController.php on line 13 and defined in C:\xampp\htdocs\project\src\project\MainBundle\Controller\Api\projectApiSecurityController.php line 40

stof commented 11 years ago

However, it is weird to extend the base controller class when defining it as a service as this base class is not meant to be used as a service. Btw, your service definition is wrong. It misses the call to setContainer to initialize the base controller.

and when using your controller as service, you need to modify your routing to reference the controller as service (see the official doc)

lsmith77 commented 11 years ago

you also need to ensure that your route is in fact using this service. i feel like you are jumping a head here. you really should try and learn the basics before getting to more advanced stuff.

a framework like Symfony2 requires a bit of studying before it can be used effectively.

lukeman83 commented 11 years ago

Thanks! I read docs and my code is:

my routing.yml:

my_security:
        class: project\MainBundle\Services\MySecurity
        arguments: ["@security.context"]

my Security class:

namespace project\MainBundle\Services\MySecurity;

use Symfony\Component\Security\Core\SecurityContextInterface;

class MySecurity
{
    protected $securityContext;

    public function __construct(SecurityContextInterface $securityContext)
    {
        $this->securityContext = $securityContext;
    }

}

in my controller:

$user=$this->container->get('my_security')->getToken()->getUser();

the error is:

he autoloader expected class \"project\MainBundle\Services\MySecurity\" to be defined in file \"C:\xampp\htdocs\project\/src\/\project\MainBundle\Services\MySecurity.php\". The file was found but the class was not in it, the class name or namespace probably has a typo.

stof commented 11 years ago

your class in the file is project\MainBundle\Services\MySecurity\MySecurity, not project\MainBundle\Services\MySecurity because your naemspace is wrong (as said by the error message btw).

and if you want to get it from the container, you don't need to create a wrapper around security.context. You can get it directly.

lukeman83 commented 11 years ago

Do you mean I need to use?

$user=$this->container->get('security.context')->getToken()->getUser();
Baachi commented 11 years ago

@lukeman83 Yes.

lukeman83 commented 11 years ago

Ok, so I used it in my controller

$user=$this->container->get('security.context')->getToken()->getUser();
$id=$user->getId();

but the error is:

Call to a member function getId() on a non-object

Baachi commented 11 years ago

Than you are not authenticated.

lukeman83 commented 11 years ago

Ok.... So the last problem is authentication by wsse. I received 403 Forbidden Response.

Baachi commented 11 years ago

You haven't the required privileges to visit this site. Maybe you are not authenticated?

lukeman83 commented 11 years ago

Yes, but I don't understand what I need to change.

my security.yml is

imports:    
    - { resource: facebookParameters.ini }

jms_security_extra:
    secure_all_services: false
    expressions: true

security:
    providers:
        my_fos_facebook_provider:
            id: my.facebook.user            
        fos_userbundle:
            id: fos_user.user_manager
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512
    firewalls:
        wsse_secured:
            pattern:   ^/api/.*
            stateless:    true
            wsse:
                nonce_dir: null
                lifetime: 5184000
                provider: fos_userbundle
            anonymous:    true

        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: form.csrf_provider
                check_path: fos_user_security_check
                login_path: fos_user_security_login
                default_target_path: homepage
            fos_facebook:
                app_url: %facebookAppUrl%
                server_url: %facebookServerUrl%
                login_path: fos_user_security_login
                check_path: _security_check
                default_target_path: homepage
                provider: my_fos_facebook_provider
            logout:
                handlers: ["fos_facebook.logout_handler"]
            anonymous:    true
            switch_user: true

    access_control:   
        - { path: ^/$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/mobile, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/login_api, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/security, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/_, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/reset, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/send-email, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/.*, role: ROLE_USER }
        - { path: ^/admin/, role: ROLE_ADMIN }

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

my config.yml is

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: facebookParameters.ini }
    - { resource: websiteParameters.ini }

services:
    my.facebook.user:
        class: project\UserBundle\Security\User\Provider\FacebookProvider
        arguments:
            facebook: "@fos_facebook.api"
            userManager: "@fos_user.user_manager"
            validator: "@validator"
            container: "@service_container" 
....

framework:
    #esi:             ~
    translator:      { fallback: %locale% }
    secret:          %secret%    
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: %kernel.debug%
    form:            true
    csrf_protection: true
    validation:      { enable_annotations: true }
    templating:      { engines: ['twig'] } #assets_version: SomeVersionScheme
    default_locale:  %locale%
    trust_proxy_headers: false # Should Request object should trust proxy headers (X_FORWARDED_FOR/HTTP_CLIENT_IP)
    session:         ~        

.....

# Assetic Configuration
assetic:
    debug:          %kernel.debug%
    use_controller: false
    bundles:        [ ]
    #java: /usr/bin/java
    filters:
        cssrewrite: ~
...

fos_user:
    db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
    firewall_name: wsse_secured #fos_secured
    user_class: project\UserBundle\Entity\User
    registration:
            form:
                type: project_user_registration
            confirmation:
                enabled:    true

    from_email:
        address:        %mailer_user%
        sender_name:    %sender_name%

fos_facebook:
      file:   %kernel.root_dir%/../vendor/facebook/php-sdk/src/base_facebook.php
      alias:  facebook
      app_id: %facebookAppId%
      secret: %facebookAppSecret%
      cookie: true
      permissions: [email, user_birthday, user_location, user_hometown, read_friendlists, user_relationships]   

jms_security_extra:
    secure_all_services: false
    enable_iddqd_attribute: false
    expressions: true

sensio_framework_extra:
    router:  { annotations: true }
    request: { converters: true }
    view:    { annotations: false }  # More info at https://github.com/FriendsOfSymfony/FOSRestBundle/issues/95
    cache:   { annotations: true }

jms_aop:
    cache_dir: %kernel.cache_dir%/jms_aop

fos_rest:
    view:
        view_response_listener: true
        failed_validation: HTTP_BAD_REQUEST
        default_engine: php
        formats:
            json: true
            xml: true
            rss: false
    format_listener:
        prefer_extension: true
    body_listener:
        decoders:
            json: fos_rest.decoder.json
    param_fetcher_listener: true
    allowed_methods_listener: true

# Mopa Rackspace Cloud Files configuration
mopa_wsse_authentication:
    provider_class: Mopa\Bundle\WSSEAuthenticationBundle\Security\Authentication\Provider\WsseAuthenticationProvider
    listener_class: Mopa\Bundle\WSSEAuthenticationBundle\Security\Firewall\WsseListener
    factory_class: Mopa\Bundle\WSSEAuthenticationBundle\Security\Factory\WsseFactory

I'm testing it with this Dev HTTP Client extension (https://chrome.google.com/webstore/detail/aejoelaoggembcahagimdiliamlcdmfm) and the Checking the Restful API section (of https://github.com/sdiaz/FOSRestBundleByExample/blob/master/app/Resources/doc/index.md)

stof commented 11 years ago

Well, maybe you are anonymous. Your firewall allows it. And in such case, $user will be a string.

lukeman83 commented 11 years ago

Ok I changed firewall properties:

firewalls:
        wsse_secured:
            pattern:   ^/api/.*
            stateless:    true
            wsse:
                nonce_dir: null
                lifetime: 5184000
                provider: fos_userbundle
            anonymous:    false

I try to use my api in this way:

First I do a POST request:

localhost/project/web/app_dev.php/security/token/create?_username=a@a.it&_password=aaa

HEADERS:

Accept : application/json Content-Type : application/x-www-form-urlencoded

I receive response 200 OK: BODY: { "WSSE":"UsernameToken Username=\"a@a.it\", PasswordDigest=\"R82hdPWyV3PoTliW5O1aoSkKRZk=\", Nonce=\"YmUwZDVhNDliNzZiM2QzZA==\", Created=\"2013-05-23T07:57:03+02:00\"" }

After I call my api with a GET request:

localhost/project/web/app_dev.php/api/something

HEADERS:

Authorization : WSSE profile="UsernameToken" X-wsse : UsernameToken Username=\"a@a.it\", PasswordDigest=\"R82hdPWyV3PoTliW5O1aoSkKRZk=\", Nonce=\"YmUwZDVhNDliNzZiM2QzZA==\", Created=\"2013-05-23T07:57:03+02:00\" ACCEPT : application/json

I receive response 403 FORBIDDEN.

What is the error?

stof commented 11 years ago

I have no experience with WSSE. I suggest you check your logs to see where the authentication failed

lukeman83 commented 11 years ago

Someone can help me please?

ghost commented 11 years ago

how did this turn into a support thread?

lukeman83 commented 11 years ago

What do you mean?

ghost commented 11 years ago

this issue tracker is meant to track issues/bugs with FosRestBundle, not help you learn how to use symfony. The mailing list is a much better place to ask for help with using the code.

lukeman83 commented 11 years ago

I'm agree with you. I'm sorry! This is a problem linked with rest API and for this reason I ask help here.