7
votes

I am trying to combine FOSUserBundle and HWIOAuthBundle following articles like https://gist.github.com/danvbe/4476697. However, I do not want the automatic registration of OAuth2 authenticated new users: additional information should be provided by the user.

Desired result

I would for example want the following information for a registered user:

  • (Username, although I'd rather just use e-mail)
  • Display name (required)
  • Profile picture (required)
  • Email address (required if no Facebook-id)
  • Password (required if no Facebook-id)
  • Facebook-id (required if no email address)

Now, when a user authenticates through Facebook and the user does not exist yet, I want a registration form to fill out the missing information (display name and profile picture). Only after this, the new FOSUser should be created. In most tutorials, fields like Profile picture and Email address are automatically populated with the Facebook information. This is not always desirable nor possible.

Also, think of things like accepting Terms of Agreement and rules you wish to show before the user is created.

Possible approaches

A solution would be, I think, to create a new sort-of AnonymousToken, the OAuthenticatedToken, which holds the relevant OAuth2 information but does not count an authenticaton. Then, make all pages check for this kind of authentication and let other pages redirect to OAuth-registration-page. However, this seems an unnecessarily complicated solution to me.

Another solution would probably be to write the code from scratch and not use the two bundles mentioned. I really hope this is not necessary.

Q: How can I insert the registration-completion-code in the rest of the login flow?

(I'd love to share some code, but since it's the very concept I need help at, I don't have a lot to show.)

Edit: Solution

Following Derick's adivce, I got the basics working like this:

The Custom user provider saves the information (sadly, no access to the raw token so I cannot yet log the user in after registering):

class UserProvider extends FOSUBUserProvider {

    protected $session;

    public function __construct(Session $session, UserManagerInterface $userManager, array $properties) {
        $this->session = $session;
        parent::__construct( $userManager, $properties );
    }

    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        try {
            return parent::loadUserByOAuthUserResponse($response);
        }
        catch ( AccountNotLinkedException $e ) {
            $this->session->set( 'oauth.resource', $response->getResourceOwner()->getName() );
            $this->session->set( 'oauth.id', $response->getResponse()['id'] );
            throw $e;
        }
    }

}

Custom failure handler:

<?php
// OAuthFailureHandler.php
class OAuthFailureHandler implements AuthenticationFailureHandlerInterface {

    public function onAuthenticationFailure( Request $request, AuthenticationException $exception) {

        if ( !$exception instanceof AccountNotLinkedException ) {
            throw $exception;
        }

        return new RedirectResponse( 'fb-register' );

    }

}

Both are registered as a service:

# services.yml
services:
    app.userprovider:
        class: AppBundle\Security\Core\User\UserProvider
        arguments: [ "@session", "@fos_user.user_manager", {facebook: facebookID} ]
    app.oauthfailurehandler:
        class: AppBundle\Security\Handler\OAuthFailureHandler
        arguments: ["@security.http_utils", {}, "@service_container"]

And configured in security config:

# security.yml
security:
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email
    firewalls:
        main:
            form_login:
                provider:            fos_userbundle
                csrf_provider:       form.csrf_provider
                login_path:          /login
                check_path:          /login_check
                default_target_path: /profile
            oauth:
                login_path:          /login
                check_path:          /login_check
                resource_owners:
                    facebook:        hwi_facebook_login
                oauth_user_provider:
                    service:         app.userprovider
                failure_handler:     app.oauthfailurehandler
            anonymous:    true
            logout:
                path:           /logout
                target:         /login

At /fb-register, I let the user enter a username and save the user myself:

/**
 * @Route("/fb-register", name="hwi_oauth_register")
 */
public function registerOAuthAction(Request $request) {

    $session = $request->getSession();

    $resource = $session->get('oauth.resource');
    if ( $resource !== 'facebook' ) {
        return $this->redirectToRoute('home');
    }

    $userManager = $this->get('fos_user.user_manager');
    $newUser = $userManager->createUser();

    $form = $this->createForm(new RegisterOAuthFormType(), $newUser);
    $form->handleRequest($request);

    if ( $form->isValid() ) {

        $newUser->setFacebookId( $session->get('oauth.id') );
        $newUser->setEnabled(true);

        $userManager->updateUser( $newUser );

        try {
            $this->container->get('hwi_oauth.user_checker')->checkPostAuth($newUser);
        } catch (AccountStatusException $e) {
            // Don't authenticate locked, disabled or expired users
            return;
        }

        $session->remove('oauth.resource');
        $session->remove('oauth.id');
        $session->getFlashBag()
            ->add('success', 'You\'re succesfully registered!' );

        return $this->redirectToRoute('home');
    }

    return $this->render( 'default/register-oauth.html.twig', array(
        'form' => $form->createView()
    ) );

}

The user is not logged in afterwards, which is too bad. Also, the normal fosub functionality (editing profile, changing password) does not work out of the box anymore.

I'm simply using the username as the displayname, not sure why I didn't see that before.

1
Did you found the solution?Igor Mancos
Mostly; I've edited it in the question, and it seems to work almost as desired. It's been some time though, don't know exactly how it ended.TacoV

1 Answers

1
votes

Step 1: Create your own user provider. Extend the OAuthUserProvider and customize to your needs. If the user successfully oauthed in, throw a specific exception (probably the accountnotlinkedException) and toss all relevant data about the login somewhere

Step 2: Create your own authentication failure handler. Check to make sure the error being thrown is the specific one you threw in step 1. In here you will redirect to your fill in additional info page.

This is how to register you custom handlers:

#security.yml
firewall:
    main:
        oauth:
            success_handler: authentication_handler
            failure_handler: social_auth_failure_handler

#user bundle services.yml (or some other project services.yml)
services:
    authentication_handler:
        class: ProjectName\UserBundle\Handler\AuthenticationHandler
        arguments:  ["@security.http_utils", {}, "@service_container"]
        tags:
            - { name: 'monolog.logger', channel: 'security' }

    social_auth_failure_handler:
        class: ProjectName\UserBundle\Handler\SocialAuthFailureHandler
        arguments:  ["@security.http_utils", {}, "@service_container"]
        tags:
            - { name: 'monolog.logger', channel: 'security' }

Step 3: Create your fill in additional info page. Pull all relevant data that you stored back in step 1 and create the user if everything checks out.