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.

Adding Tumblr to HWIOAuthBundle

Tumblr HWIOAuthI recently needed to add in Tumblr social authentication to a Symfony project. I made use of the excellent HWIOAuthBundle, which made the whole process a breeze.

Unfortunately, out of the box, there is no direct support for Tumblr.

However, no fear, as manually adding support in is simple enough.

Follow the installation instructions through to the end of Step 2. At the bottom of the section on Configuring Resource Owners is the option for ‘Others‘.

To get this to work, it’s mainly a bit of back and forth between the Tumblr API docs and your config.yml file, so let me spare you the time:

hwi_oauth:
    # list of names of the firewalls in which this bundle is active, this setting MUST be set
    firewall_name: main # important to change this to match the firewall you are adding hwioauth too
    resource_owners:
        tumblr:
            type:                oauth1
            client_id:           your_tumblr_api_client_id_here
            client_secret:       your_tumblr_api_client_secret_here
            access_token_url:    https://www.tumblr.com/oauth/access_token
            authorization_url:   https://www.tumblr.com/oauth/authorize
            request_token_url:   https://www.tumblr.com/oauth/request_token
            infos_url:           https://api.tumblr.com/v2/user/info
            scope:               "read"
            user_response_class: HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse
            paths:
                identifier:     response.user.name
                nickname:       response.user.name
                realname:       response.user.name

The paths section is interesting.

We must specify at least these three for HWIOAuthBundle to not error out.

What I’m doing here is using a dotted array access format. Kinda confusing sounding, but really what this is saying is : $response[‘user’][‘name’] .

Why the same data for each field?

Well, Tumblr doesn’t return much in the way of account data here.

You can see the full output from Tumblr here:

PathUserResponse.php on line 122:
array:2 [▼
  "meta" => array:2 [▶]
  "response" => array:1 [▼
    "user" => array:5 [▼
      "name" => "futuristicallycrookedbread"
      "likes" => 0
      "following" => 6
      "default_post_format" => "html"
      "blogs" => array:1 [▼
        0 => array:29 [▼
          "title" => "Untitled"
          "name" => "futuristicallycrookedbread"
          "posts" => 4
          "url" => "http://futuristicallycrookedbread.tumblr.com/"
          "updated" => 1441562102
          "description" => ""
          "is_nsfw" => false
          "ask" => false
          "ask_page_title" => "Ask me anything"
          "ask_anon" => false
          "followed" => false
          "can_send_fan_mail" => true
          "is_blocked_from_primary" => false
          "share_likes" => true
          "likes" => 0
          "twitter_enabled" => false
          "twitter_send" => false
          "facebook_opengraph_enabled" => "N"
          "tweet" => "N"
          "facebook" => "N"
          "followers" => 0
          "primary" => true
          "admin" => true
          "messages" => 0
          "queue" => 0
          "drafts" => 0
          "type" => "public"
          "subscribed" => false
          "can_subscribe" => false
        ]
      ]
    ]
  ]
]

This is the result of using Symfony’s VarDumper component on line 122 of PathUserResponse.php .

How did I know to add the dump($response); statement in there?

Well, we specified in the config.yml file that we are expecting a User Response to be:

user_response_class: HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse

I just opened up that file and added in the dump, using the $response variable that had been defined earlier on line 113.

There is a little bit of extra config in security.yml :

security:
    providers:
        social_user_provider:
             id: oauth_user_provider

    firewalls:
        main:
            anonymous: ~
            oauth:
                resource_owners:
                    tumblr:        "/login/check-tumblr"
                login_path:        /login
                use_forward:       false
                failure_path:      /login
                oauth_user_provider:
                    service: oauth_user_provider

    access_control:
        - { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/connect, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, role: ROLE_USER }

Alongside the other HWIOAuth routes, you will need to add in one for Tumblr:

# app/config/routing.yml - or where ever you are storing routes
tumblr_login:
    path: /login/check-tumblr

The oauth_user_provider  service must exist, so here is my definition:

# services.yml (or where ever)
services:
    oauth_user_provider:
        class: AppBundle\Model\OAuthUserProvider

This should work for other social auth providers, with only a little modification. As much as this looks like loads of config (hey, it’s a Symfony project!), it’s actually really pretty easy to add in new social providers. I was pleasantly surprised.

ParseException: The YAML value does not appear to be valid UTF-8

Sometimes Symfony throws up the most peculiar of error messages.

I had a situation where I had two config (.yml) files, one environment overriding the other.

Now, what was particularly confusing about this situation was that I had copied and pasted the config (not very DRY) from the first file to the second.

This wasn’t a new project – both files had been around for a long time, but it was the first time I was using the second file properly.

The idea in this instance was the second config file would inherit all the first config files settings, but over rule the ones I had changed.

As soon as I did this, however, I got the following:

symfony yaml parsing error

code error text for Google’s benefit:

Whoops, looks like something went wrong.

1/1ContextErrorException: Warning: htmlspecialchars(): Invalid multibyte sequence in argument in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php line 302
in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php line 302
at ErrorHandler->handle('2', 'htmlspecialchars(): Invalid multibyte sequence in argument', '/mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php', '302', array('args' => array(array('string', '')), 'result' => array(), 'key' => '0', 'item' => array('string', '')))
at htmlspecialchars('', '11', 'UTF-8') in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php line 302
at ExceptionHandler->formatArgs(array(array('string', ''))) in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php line 158
at ExceptionHandler->getContent(object(FlattenException)) in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php line 117
at ExceptionHandler->createResponse(object(ParseException)) in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Debug/ExceptionHandler.php line 74
at ExceptionHandler->handle(object(ParseException))
Exception thrown when handling an exception (Symfony\Component\Debug\Exception\FlattenException: The YAML value does not appear to be valid UTF-8.)

1/1ParseException: The YAML value does not appear to be valid UTF-8.
in /mnt/project/my-project/vendor/symfony/symfony/src/Symfony/Component/Yaml/Parser.php line 59

A classic Symfony blow up.

Now, whilst this may be a Symfony project, it is not a Symfony problem!

The problem is the encoding of the file itself. This sounds like it might be a total pain to fix, but if you are using PHPStorm it’s actually very, very easy.

How to Fix ParseException: The YAML value does not appear to be valid UTF-8

Open the file in PHPStorm.

Take a look in the bottom right corner:

phpstorm-file-encoding-selection

Notice – I am using Windows here, so the default file encoding type is what caught me out.

Click the text ‘windows-1252’ or whatever you have here that is not UTF-8.

Select UTF-8.

You may get a little pop up warning you that your file will be converted to UTF-8.

Take a back up if you aren’t sure, but otherwise, convert it.

Upload the file (or do nothing if using shared mounts in Vagrant), and success, we have captured the enemies intelligence.