HWI OAuth Bundle – Custom Registration Form

I’ve been busy integrating a new application with third party / social authentication providers, specifically Tumblr, but this applies to pretty much any service as best I can tell.

In this post I want to cover how, and why, you can override the default implementation HWIOAuthBundle’s connect  configuration to specify your own account_connector.

I am using HWIOAuthBundle a little differently from how it is primarily designed to work.

HWIOAuthBundle is all about offering users of your site the ability to log in using their trusted third party / social media credentials rather than having to create yet another user account to use your site. You see this functionality a lot on the web these days, and it’s no surprise that Symfony has a bundle to deal with this.

Configuration of HWIOAuthBundle isn’t that hard, but the documentation doesn’t hand hold you as much as you might find with other bundle documentation. If you find yourself stuck or confused, the project issue tracker on Github is a great place to start digging.

Why Use A Custom account_connector ?

There are likely a good few reasons to do this.

In my case, I don’t want users to be able to register on the site with social media credentials. That in itself comes with its own set of headaches.

Users of my site must create an account the old fashioned way. This is handled through the brilliant FOSUserBundle.

Then, once authenticated, the user can connect  their account to one or more of the various social platforms – Twitter, Facebook, Google+ and what have you.

The curve ball in my situation comes from the above, but also that a User account can have many sub accounts. Think of it as in an agency looking after multiple client accounts, all from one login. None of the client accounts should be aware of the other client accounts under management, but the ‘master’ User account needs to seamlessly switch between them all.

The problem: when connecting to a new social account, how do you determine which client account to connect to which social account?

Out of the box, HWIOAuthBundle will return the ‘master’ User account object, and the social media site’s response object. From there, there is no simple way to determine the ‘active’ client account.

I didn’t want to start managing application state through a Session, as I could envisage a situation where a User may try and connect multiple accounts in multiple tabs, and end up with the wrong social media account assigned to the wrong client account. Messy.

Sending the relevant data via the URL seems like an easy solution to this problem. Perhaps though, not the right one.

Having given this a bit of thought, the next step seems to be having an input of type choice  on the connect page.

There is no bundle documentation (that I could find) to actually do this in their expected way (as in FOSUserBundle docs), though thankfully there is a handy Reference Configuration.

This is the relevant section:

# app/config.yml
hwi_oauth:
    # if you want to use 'connect' and do not use the FOSUB integration, configure these separately
    connect: ~
#        confirmation: true # should show confirmation page or not
#        registration_form_handler: my_registration_form_handler
#        registration_form: my_registration_form
#        account_connector: my_link_provider # can be the same as your user provider

As you can see, by default, we go with the built in / provided implementations.

We can override any of the default Services by specifying other Symfony Services, but exactly how these Services are configured is not explicitly described.

But what about overriding the template? Why isn’t that listed?

Well, that’s pretty common in Symfony, but it’s definitely a valid question.

Not one to turn down a challenge, I decided to do a little code diving, and see if I couldn’t come up with a DIY solution.

My Implementation

My idea is to have a modified connection form.

The connection form is displayed midway through the ‘connection’ process.

Remember, the User has already logged in to my application via their FOSUserBundle credentials.

Then, they can choose to ‘connect’ with a configured Social Media service – let’s say Tumblr, for sake of this example.

They click ‘Connect to Tumblr’, and are redirected to some URL on Twitter whereby they authenticate with their Twitter credentials (or, if already logged in, are simply shown a pop-up / modal to confirm they agree to this authorisation), and are then returned to my app with some params on the URL – their OAuth token info.

I want to save this profile info, but I need to ensure it is mapped to the correct client account under the currently logged in / locally authenticated User.

At this point, the User would have been redirected back from Tumblr to our ‘Connect’ form. Without any modification or styling, that form is going to look something like this :

hwio-oauth-connect-form

That form lives at :

/vendor/hwi/oauth-bundle/HWI/Bundle/OAuthBundle/Resources/views/Connect/connect_confirm.html.twig

We can tell it’s definitely this form by editing that template in the bundle.

I’m doing this for demo purposes, and it’s great for confirming what you already guess to be the right file, but don’t make direct edits to the bundle files (any of them), as all your changes will (potentially) be lost the next time you do a composer update on that particular bundle.

the-hwio-aiuth-connect-template-opened-in-phpstorm

Outputs:

hwio-oauth-bundle-slightly-modified-connect-formMy plan is to therefore add a checkbox on that page to allow the User to connect the given social media account to one or more of their customer / sub accounts.

We will need to handle some contingency stuff here, like that new User who doesn’t check any of the boxes and clicks ‘continue’ anyway. We don’t want this to cause an error on the back end, even though we would ideally validate this before hand on the front-end.

At this point I added in a way to stop the process before any persistence. I did this by defining my own CustomerConnector class and then turning this into a Symfony Service. This is where I would put all my implementation details:

<?php

// /src/AppBundle/OAuth/Connect/CustomerConnector.php

namespace AppBundle\OAuth\Connect;

use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class CustomerConnector extends BaseClass
{
    public function connect(UserInterface $user, UserResponseInterface $response)
    {
        dump($user);
        dump($response);
        exit('connect');
    }
}
# /app/config/services.yml
services:
    customer_connector:
        class: AppBundle\OAuth\Connect\CustomerConnector
        arguments: [@fos_user.user_manager, {}]
# app/config/config.yml
hwi_oauth:
    firewall_name: main
    connect:
        account_connector: customer_connector

This would let me go through the same process over and over, but die before anything further happens, showing me all the good stuff we would need to think about persistence.

One thing at a time.

Adding in the checkboxes

The next major hurdle is where to start with adding in our own Form implementation.

I’m not spotting a hugely easy way to achieve what we are trying to do without hacking a little.

The issue we have is that HWIOAuthBundle expects to be dealing with a User throughout the lifecycle of the connection process.

There is no easy way to override this – that I can see – and I don’t want to implement the 34 required methods of Symfony’s UserInterface on my Customer entities.

However, as it happens, the ConnectController doesn’t seem to care what we put on our form.

Good news, we have ourselves an entry point.

This means we can add extra stuff (basic HTML form elements) to the /vendor/hwi/oauth-bundle/HWI/Bundle/OAuthBundle/Resources/views/Connect/connect_confirm.html.twig  template.

But, rather than directly edit the template in the bundle – which we have already decided is a bad idea – we are going to use a little Symfony bundle inheritence magic to override the template with our own at:

/app/Resources/HWIOAuthBundle/views/Connect/connect_confirm.html.twig

This is a pretty cool Symfony feature, so if you haven’t used it before, check out the official documentation. It can come in handy for times like this.

Our template is going to replicate all the existing layout / functionality, but add in a little raw HTML to further our own goals:

{% extends 'HWIOAuthBundle::layout.html.twig' %}

{% block hwi_oauth_content %}
    <h3>{{ 'header.connecting' | trans({}, 'HWIOAuthBundle')}}</h3>
    <div class="row">
        <div class="span6">
            <p>{{ 'connect.confirm.text' | trans({'%service%': service | trans({}, 'HWIOAuthBundle'), '%name%': userInformation.realName}, 'HWIOAuthBundle') }}</p>
            <p>
            <form action="{{ path('hwi_oauth_connect_service', {'service': service, 'key': key}) }}" {{ form_enctype(form) }} method="POST" class="fos_user_registration_register">
                {{ form_widget(form) }}
                <input type="checkbox" name="accounts[]" value="value1"> value1
                <input type="checkbox" name="accounts[]" value="value2"> value2
                <div>
                    <button type="submit" class="btn btn-primary">{{ 'connect.confirm.submit' | trans({}, 'HWIOAuthBundle') }}</button>
                    <a href="{{ path('hwi_oauth_connect') }}" class="btn">{{ 'connect.confirm.cancel' | trans({}, 'HWIOAuthBundle') }}</a>
                </div>
            </form>
            </p>
        </div>
        <div class="span6">
            {% if userInformation.profilePicture is defined and userInformation.profilePicture is not empty %}
                <img src="{{ userInformation.profilePicture }}" />
            {% endif %}
        </div>
    </div>
{% endblock hwi_oauth_content %}

We’ve added in the raw HTML above, using two simple input elements to fake our data.

The basics are important – we have set the name=”accounts[]”  property on both, so that the responses are grouped together and we get an array result.

However, we still have a problem.

Even though we have added the checkboxes, and they will be being properly appended to our Symfony Request , the ConnectController  is not passing the Request object to our CustomerConnector :

// /vendor/hwi/oauth-bundle/HWI/Bundle/OAuthBundle/Controller/ConnectController.php#L209
$this->container->get('hwi_oauth.account.connector')->connect($currentUser, $userInformation);

I see this a lot, but remember, you can’t just ask for a Request object inside a Symfony Service in the same way you can in a Symfony Controller.

We do control this service. Whilst it may not directly obvious, this is actually referring to our account_connector.

We can’t change the method signature (connect(UserInterface $user, UserResponseInterface $response) ), and even if we could, that would mean we would also have to duplicate the entire ConnectController::connectServiceAction() , which we really don’t want to do.

Instead, we can use another bit of functionality available to us in Symfony: Setter Injection.

We can use setter injection to pass in Symfony’s RequestStack and then access the extra data direct from the Request object that way.

With all this configured, our CustomerConnector now looks like this:

<?php

namespace AppBundle\OAuth\Connect;

use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;

class CustomerConnector extends BaseClass
{
    /**
     * @var RequestStack
     */
    private $requestStack;

    public function injectRequestStack(RequestStack $requestStack)
    {
        $this->requestStack = $requestStack;
    }

    public function connect(UserInterface $user, UserResponseInterface $response)
    {
        dump($user);
        dump($response);
        dump($this->requestStack->getMasterRequest());
        exit('connect');
    }
}
# services.yml

services:
    customer_connector:
        class: AppBundle\OAuth\Connect\CustomerConnector
        arguments: [@fos_user.user_manager, {}]
        calls:
            - [ "injectRequestStack", [@request_stack]]

Which looks like this on screen:

result-of-connect

We need to do a little modification to the template. The default translation text is incorrect, and we need to pass in the real data, but those are both quick Twig template tweaks.

We will likely want to add some Bootstrap specific (other front end design frameworks are available ;)) CSS to our form template also.

After that, we can move on to the logic that actually handles associating the oAuthToken data with our chosen accounts.

Review

I’m not 100% happy with this implementation.

That I’ve not had to overwrite any controller action is good.

But I now have to keep my template up to date with whatever implementation changes come in HWIOAuthBundle. Likely this won’t change hugely, but almost inevitably, at some point in the future, this will break in a hard to debug manner.

Also, I’m relying on the HWIOAuthBundle controller behaving as I expect it.

This feels quite shaky. Covering the code sufficiently with tests will be crucial, but the tests themselves will be slow, difficult to write, and brittle.

This approach is pragmatic. I am happy that this is the best solution to the problem given the time frame, and the fact that I am working on a personal project / MVP.

If you know of a better way to implement this, please do leave a comment.

Symfony 2 Form: A Short Guide

A web form or a HTML form is basically a page that allows a user to enter information that is then sent to a server for processing.

This is a short guide to the Symfony 2 form. Symfony has a very powerful form component that makes using forms simpler when compared to HTML forms, although forms can still be very challenging to get your head around and learn.

Within Symfony there is a standalone form component library that can also be used with other non-Symfony projects.

Symfony 2 gives us a wide range of ways that we can customise how a form is rendered. Form fragments can be used to render just one part of a Symfony 2 form, or to render each part of the form.

Symfony 2 form elements can include:

  • Simple text input
  • Text area
  • Select drop down list
  • Checkboxes
  • Radio buttons
  • File selection
  • Reset button
  • Submit button

Creating a form

We can write recipes with form objects using the form builder. You can also import a form theme to customise your form, or use the default Symfony 2 form theme that comes as standard with Symfony.

When using forms in a template in Symfony, you can use functions (for rendering each bit of the form) and variables (less often used, to obtain any information about any field).

To get started with forms, watch our video on YouTube, an introduction to forms in Symfony 2.

This video covers a range of topics including:

Creating a basic Symfony 2 form

It covers building the form inside our controller and rendering the form in our view.

How to handle the submission of the form back to our controller (where user presses submit).

image-symfony-2-form.png

Rendering your form

There is noob mode that involves pasting it in, and real world mode that is more indepth and where your form gets customised.

Changing the action and method of a form

This covers posting to GET and where your data is being posted to.

Form classes

Creating reusable forms is discussed, and how to separate our form logic from our controller.

Form validation

You shouldn’t trust the raw data input from a user. Symfony provides two methods of form validation, there is the front end and server side. Front end shouldn’t be relied upon and you should ideally always implement server side validation. How to do this is explained.

How to prevent cross site request forgeries (CSRF)

This involves a single line of code and is not as complicated as it may sound.

More resources to learn about the Symfony 2 form:

http://symfony.com/doc/current/book/forms.html

https://github.com/symfony/Form

The best way to learn is by doing so start practicing!