Integrate HWIOAuthBundle With FOSUserBundle

HWIOAuthBundle is a great Symfony2 bundle that provides way to integrate web services that implements OAuth1.0 and OAuth2 as user authentication system. Once configured you can add infinite amount of web services as authentication source.

After user authentication it is better to fetch user information from the web service and store them in DB so that the user does not have to input profile information again. In following section I will outline step by step instruction on how to configure HWIOAuthBundle and integrate FOSUserBundle user provider using fosub_bridge implemented in HWIOauthBundle. For web service Github OAuth api used.

HWIOAuthBundle uses Buzz curl client to communicate with web services. Buzz by default enables SSL certificate check. On some server CA certificate information may not exist. To add CA certificate info download cacert.pem from this page and set curl.cainfo php ini variable to the location of cacert.pem e.g

php.ini
1
curl.cainfo = /path/to/cacert.pem

Then register application of the web service you want to use for authentication. For this post I have used Github for its simplicity. You can create application from here. Your registration form may look like following,

After successful application creation you will be redirected to application page where you will see client ID and Client Secret fields set for the application. They will be used later.

Add the bundle info in composer.json and issue php composer.phar update --prefer-dist command.

composer.json
1
2
3
4
5
6
7
{
    require: {
        //...
        "friendsofsymfony/user-bundle": "v1.3.2",
        "hwi/oauth-bundle": "0.3.*@dev",
    }
}

Enable the bundles in app/AppKernel.php,

app/AppKernel.php
1
2
3
4
5
6
7
8
public function registerBundles()
{
    $bundles = array(
        // ...
        new FOS\UserBundle\FOSUserBundle(),
        new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
    );
}

Now setup FOSUserBundle. For this tutorial I will only show user entity creation and configuration. For other setup refer to the documentation.

In one of your bundle add entity class with field information. After that add a entity field named githubID which maps to the github user id. Minimal entity class is given bellow.

src/YourVendor/YourBundle/Entity/User.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

namespace YourVendor\YourBundle\Entity;

use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @var string
     *
     * @ORM\Column(name="github_id", type="string", nullable=true)
     */
    private $githubID;


    public function __construct()
    {
        parent::__construct();
        // your own logic
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

}

Add routes of FOSUserBundle in app/config/rouging.yml. Please note that I am securing parts of the site that matches with ^/secure_area url pattern. So appropriate prefix was added in this case. To apply it in root url just remove /secure_area portion in all occurrences.

app/config/routing.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#...
fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"
    prefix: /secure_area

fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /secure_area/profile

fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /secure_area/register

fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /secure_area/resetting

fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /secure_area/profile

Add entity info in the app/config/config.yml

app/config/config.yml
1
2
3
4
5
6
7
8
#...
fos_user:
    db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
    firewall_name: secure_area
    user_class: YourVendor\YourBundle\Entity\User
    registration:
        confirmation:
            enabled:    false # change to true for required email confirmation

Then in app/config/security.yml add encoders and providers information.

app/config/security.yml
1
2
3
4
5
6
7
8
security:
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

#...

Now setup HWIOauthBundle. Add routes of HWIOAuthBundle to app/config/routing.yml.Another route named hwi_github_login was also added which is same as the callback url given during creation of Github application. This is the url which will be intercepted by the firewall to check authentication.

app/config/routing.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
hwi_oauth_redirect:
    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
    prefix:   /secure_area/connect

hwi_oauth_login:
    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
    prefix:   /secure_area/connect

hwi_oauth_connect:
    resource: "@HWIOAuthBundle/Resources/config/routing/connect.xml"
    prefix:   /secure_area/connect

hwi_github_login:
    pattern: /secure_area/login/check-github

Now setup the security firewall.

app/config/security.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
security:
    #...
    firewalls:
        #...
        secure_area:
            pattern: ^/secure_area

            oauth:
                failure_path: /secure_area/connect
                login_path: /secure_area/connect
                check_path: /secure_area/connect
                provider: fos_userbundle
                resource_owners:
                    github:           "/secure_area/login/check-github"
                oauth_user_provider:
                    service: hwi_oauth.user.provider.fosub_bridge

            anonymous:    true
            logout:
                path:           /secure_area/logout
                target:         /secure_area/connect #where to go after logout

    #...

    access_control:
        #....
        - { path: ^/secure_area/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/secure_area/connect, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/secure_area, roles: ROLE_USER }

In firewalls section a new firewall named secure_area with OAuth provider named oauth is added which handles ^/secure_area url pattern. In resource_owners section of the OAuth provider intercept url for the Github resource owner is provided. It is same as the callback url given during Github application creation.

In later access_control section path matching ^/secure_area/connect and ^/secure_area/login pattern moved out of secure area.

User provider of the OAuth authentication provider is fos_userbundle which was setup previously. As user provider is FOSUserBundle, built-in hwi_oauth.user.provider.fosub_bridge service was set as oauth_user_provider. If you want to set it to your custom user provider you have to implement OAuthAwareUserProviderInterface.

Now setup app/config/config.yml.

app/config/config.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#...
hwi_oauth:
    # name of the firewall in which this bundle is active, this setting MUST be set
    firewall_name: secure_area
    connect:
        confirmation: true
        #account_connector: hwi_oauth.user.provider.fosub_bridge
        #registration_form_handler: hwi_oauth.registration.form.handler.fosub_bridge
        #registration_form: fos_user.registration.form

    resource_owners:
        github:
            type:                github
            client_id:           <client_id>
            client_secret:       <client_secret>
            scope:               "user:email"

    fosub:
        # try 30 times to check if a username is available (foo, foo1, foo2 etc)
        username_iterations: 30

        # mapping between resource owners (see below) and properties
        properties:
            github: githubID

The value of firewall_name is same as the name of the firewall with OAuth provider setup in app/config/security.yml.

In resource_owners section OAuth information were added. The value of client_id and client_secret are the values set by Github after the creation of the application. For configuration of other resource owners see the documentation.

Since FOSUserBundle were used as user provider, fosub section were added. In properties section githubID entity field was set as value of github config field.

The connect section connects HWIOAuthBundle to the registration system of Symfony. It also links existing logged in users to the authenticated service. Note that simply adding connect: ~ would be enough to link HWIOAuthBundle to the registration system. For the brief explanation of the options I have added default values.

If confirmation option is set to true, user will be shown a page that will ask the user to connect the current authenticated resource to existing logged in user account. The template location is HWIOAuthBundle:Connect:connect_confirm.html.twig. To override the template see the documentation.

The value of account_connector is a user provider class that implements AccountConnectorInterFace. By default it is set to same hwi_oauth.user.provider.fosub_bridge service that was set in OAuth firewall. So if you want to add support for your custom user provider you have to extend it so that it implements AccountConnectorInterFace and OAuthAwareUserProviderInterface.

The registration_form_handler is set to hwi_oauth.registration.form.handler.fosub_bridge service. It is used during registration process and does almost same thing as default FOSUserBundle registration form handler. The difference is that it implements RegistrationFormHandlerInterface. So if you want to add your custom handler you have to extend the handler to implement RegistrationFormHandlerInterface.

The value of registration_form is same as default FOSUserBundle registration form fos_user.registration.form. It is used during registration operation. The twig template of the registration file is at HWIOAuthBundle:Connect:registration.html.twig. Override it to meet your requirement.

Then issue following commands which will generate entity setter/getter methods and save table information to DB.

command
1
2
 php app/console doctrine:generate:entities YourVendorYourBundle
 php app/console doctrine:schema:update --force

Thats all. Now go to any url matcing ^/secure_area pattern and you will be redirected to /secure_area/connect url where lists of OAuth resource owners will be shown. The twig template of the page is HWIOAuthBundle:Connect:login.html.twig. Override it to meet your requirement. After successful OAuth authentication new user will be redirected to registration page or to previous page if the user already exists.

Once first resource owner is configured adding other resource owners is very easy. Just add mapping resource owners field in the entity, add check-resource route on app/config/routng.yml, add client id and client secret to app/config/config.yml, add property mapping and add another line in resource_owners section of the app/config/security.yml.

Another bonus tip, After successful authentication you can get access token of the resource from the toke of the security.context service as HWIOAuthBundle sets OAuthToken after successful authentication. So just by adding following line

YourController.php
1
$accessToken = $this->get('security.context')->getToken()->getAccessToken();

will give you the access token with which you can do REST API call to the resource.

I have combined code example of this post and my previous post and uploaded to Github. It integrates FOSUserBundle, SonataAdminBundle, SonataUserBundle and HWIOAuthBundle. Enjoy.

Integrate FOSUserBundle and SonataUserBundle Easily

SonataUserBundle is a great extension of SonataAdminBundle that provides user administration features by integrating FOSUserBundle user provider/management bundle. Its default installation procedure recommends to setup SonataUserBundle as child bundle of FOSUserBundle and generate ApplicationSonataUserBundle via sonata:easy-extends:generate command. But on some cases you may not want to setup that way. For example you have setup your user entity by following the documentation of FOSUserBundle before integrating SonataAdminBundle and SonataUserBundle, you may want to override both bundles separately. In following section I will outline how to integrate SonataUserBundle with FOSUserBundle without creating child bundle of FOSUserBundle.

Default installation and setup of FOSUserBundle is same as stated in default documentation. Then install and setup SonataAdminBundle according to its documentation. Both guides are pretty well documented. So I will not duplicate them here.

Now setup SonataUserBundle. Add it to composer.json and add following line into registerBundle method of app/AppKernel.php

app/AppKernel.php
1
2
3
4
5
6
7
8
9
// app/AppKernel.php
public function registerBundles()
{

    $bundles = array(
        // other bundle declarations
        new Sonata\UserBundle\SonataUserBundle(),
    );
}

Then setup configuration, add routing and security configuration according to the documentation.

Now set value of sonata.user.admin.user.class parameter to the FQCN of the User entity which was created during FOSUserBundle setup. For example if FQCN of your user entity is YourVendor\YourBundle\Entity\User then parameter setting of app/config.yml would be

app/config/config.yml
1
2
3
parameters:
    #....
    sonata.user.admin.user.entity: YourVendor\YourBundle\Entity\User

Now create a class that extends default UserAdmin class and override configureShowFields, configureFormFields, configureDatagridFilters and configureListFields methods to add the needed user admin fields. Following is the sample extended UserAdmin class which is based on the bare bone user entity created in FOSUserBundle documentation.

src/YourVendor/YourBundle/Admin/UserAdmin.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
//src/YourVendor/YourBundle/Admin/UserAdmin.php

namespace YourVendor\YourBundle\Admin;

use Sonata\UserBundle\Admin\Model\UserAdmin as BaseUserAdmin;

use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Show\ShowMapper;

use FOS\UserBundle\Model\UserManagerInterface;
use Sonata\AdminBundle\Route\RouteCollection;


class UserAdmin extends BaseUserAdmin
{
    /**
     * {@inheritdoc}
     */
    protected function configureShowFields(ShowMapper $showMapper)
    {
        $showMapper
            ->with('General')
                ->add('username')
                ->add('email')
            ->end()
            // .. more info
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function configureFormFields(FormMapper $formMapper)
    {

        $formMapper
            ->with('General')
                ->add('username')
                ->add('email')
                ->add('plainPassword', 'text', array('required' => false))
            ->end()
            // .. more info
            ;

        if (!$this->getSubject()->hasRole('ROLE_SUPER_ADMIN')) {
            $formMapper
                ->with('Management')
                    ->add('roles', 'sonata_security_roles', array(
                        'expanded' => true,
                        'multiple' => true,
                        'required' => false
                    ))
                    ->add('locked', null, array('required' => false))
                    ->add('expired', null, array('required' => false))
                    ->add('enabled', null, array('required' => false))
                    ->add('credentialsExpired', null, array('required' => false))
                ->end()
            ;
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function configureDatagridFilters(DatagridMapper $filterMapper)
    {
        $filterMapper
            ->add('id')
            ->add('username')
            ->add('locked')
            ->add('email')
        ;
    }
    /**
     * {@inheritdoc}
     */
    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->addIdentifier('username')
            ->add('email')
            ->add('enabled', null, array('editable' => true))
            ->add('locked', null, array('editable' => true))
            ->add('createdAt')
        ;

        if ($this->isGranted('ROLE_ALLOWED_TO_SWITCH')) {
            $listMapper
                ->add('impersonating', 'string', array('template' => 'SonataUserBundle:Admin:Field/impersonating.html.twig'))
            ;
        }
    }
}

Now set the value of sonata.user.admin.user.class to the FQCN of the created UserAdmin class in app/config/config.yml, e.g

app/config/config.yml
1
2
3
4
5
6
7
#app/config/config.yml

parameters:
    # ...
    sonata.user.admin.user.class: YourVendor\YourBundle\Admin\UserAdmin

# ....

If you don’t need user group functionality you can disable it. e.g in your app/config/config.yml add following lines.

app/config/config.yml
1
2
3
4
5
6
#app/config/config.yml

services:
    sonata.user.admin.group:
        abstract: true
        public: false

Combined config.yml setting given bellow,

app/config/config.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
#app/config/config.yml

#...

parameters:
    sonata.user.admin.user.class: YourVendor\YourBundle\Admin\UserAdmin
    sonata.user.admin.user.entity: YourVendor\YourBundle\Entity\User

services:
    sonata.user.admin.group:
        abstract: true
        public: false
#...

If everything setup correctly you will see an new users row in admin/dashboard page. All user operations should work as expected. Thats all for now.