Symfony2: How to login using OAuth (HWIOAuthBundle) + custom roles (by default and loaded from DB)

luisete89 picture luisete89 · Aug 16, 2013 · Viewed 12.2k times · Source

Note: the questions are at the end of this text but I will explain in detail all the context for a better understanding.

I'm developing a Symfony2 application consisting of 3 modules:

  1. Module for students -> Needs the role ROLE_STUDENT
  2. Module for teachers -> Needs the role ROLE_TEACHER
  3. Module for administrators -> Needs the role ROLE_ADMIN

The role hierarchy is as follows:

  • ROLE_TEACHER: [ROLE_STUDENT]
  • ROLE_ADMIN: [ROLE_TEACHER]

Therefore:

A student (with ROLE_STUDENT role) only can access to all the pages of his/her module, for example:

A teacher (with ROLE_TEACHER role) can access to all the pages of the students' module and all the pages of the teachers' module, such as:

An administrator (with ROLE_ADMIN role) can access to all the pages of the students' module, all the pages of the teachers' module and the admin backend.

The system uses OAuth to authenticate students and teachers through Twitter, using the bundle HWIOAuthBundle (using the OAuthUserProvider user provider that provides the bundle).

I've achieved to successfully authenticate users but, by default, all users are automatically authenticated with the following roles: [ROLE_USER, ROLE_OAUTH_USER]


So here is what I have done. Below, I'm going to explain what I want that my app do but I don't know the way:

Steps for login using OAuth:

A user logs in to the system and automatically:

  • If the user does not exist in the database:
    - Save the nickname in the users table of the database.
    - Save the role ROLE_STUDENT (this is the role by default for me) in the users table of the database.
    - Authenticate in the system using OAuth but using the ROLE_STUDENT role.

  • If the user exists in the database:
    - Check what role has associated the user in the database.
    - Authenticate the user in the system using OAuth but using the role associated in the database (i.e.: ROLE_STUDENT or ROLE_TEACHER).

The administrator (from the administration backend) can see the list of nicknames (used by Twitter but saved in the database) and the assigned roles for each nickname. The administrator should be able to change the role of users between ROLE_STUDENT and ROLE_TEACHER.


The questions:

  1. How I can authenticate a user via OAuth (HWIOAuthBundle) with the role that I want by default (ROLE_STUDENT as I explained above)?

  2. If a nickname exists in the database with a role associated (ROLE_STUDENT or ROLE_TEACHER), how I can authenticate a user via OAuth (HWIOAuthBundle) but using the role loaded from the database?

I've been reading a lot about this subject but I'm new with Symfony2 and I don't know what is the best and easiest way to solve it.

Thank you in advance!

PS: if you have any questions or doubts about anything, I'll be glad to explain as best as possible.

Answer

ioan picture ioan · Aug 17, 2013

UPDATE:

I tried using this code in one of my recent projects and it didn't work anymore. The reason is that HWIOAuthBundle has updated a few times and the config files are not the same. I put the code above as well as login to a few other social networks in a github which you can find at HWIOAuthBundleByExample.


I have a week experience with symfony2 and in the past days that's what i was working myself. Found your question today (when i was still researching).

I'm gonna present you what I had to do, based on what data, and how i did it. After that, I'll try giving you some key links and I hope you'll manage to modelate for your needs.

My app needs Facebook login and ADMIN role. Because will only be a few administrators, I only need a few Facebook IDs when authentificating, so I store them in a yaml array. (See at the end how you can load them from database).

Here's what I did:

#/app/config.yml #the setup looks different (I need the picture and email credentials)
hwi_oauth:
  # name of the firewall in which this bundle is active, this setting MUST be set
  firewall_name: secured_area
  resource_owners:
    facebook:
      type:        facebook
      client_id:       %facebook_client_id%
      client_secret:     %facebook_client_secret%
      scope:         "email"
      infos_url:     "https://graph.facebook.com/me?fields=username,name,email,picture.type(square)"
      paths:
        email:          email
        profilepicture: picture.data.url
services: #here's where the magic happens
  hwi_oauth.user.provider.entity:
    class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider
  ib_user.oauth_user_provider:
    class: Acme\DemoBundle\Provider\Provider
    arguments: [@session, @doctrine, %admins%]

#app/security.yml
security:
  providers:
    my_custom_hwi_provider:
      id: ib_user.oauth_user_provider

  access_control:
        - { path: ^/admin, roles: ROLE_SUPER_ADMIN }



#app/parameters.yml
parameters:
  #...
  facebook_client_id:     ###
  facebook_client_secret: ###
  admins:
    - "my.facebook.id"

#Acme\DemoBundle\Provider\Provider
<?php

namespace Acme\DemoBundle\Provider;

use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Acme\DemoBundle\Entity\User;
use Acme\DemoBundle\Provider\OAuthUser;

class Provider extends OAuthUserProvider
{
    protected $session, $doctrine, $admins;
    public function __construct($session, $doctrine, $admins) {
        $this->session = $session;
        $this->doctrine = $doctrine;
        $this->admins = $admins;
    }

    public function loadUserByUsername($username)
    {
        return new OAuthUser($username, $this->isUserAdmin($username)); //look at the class below
    }

    private function isUserAdmin($nickname)
    {
        return in_array($nickname, $this->admins);
    }

    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        //data from facebook response
        $facebook_id = $response->getUsername();
        $nickname = $response->getNickname();
        $realname = $response->getRealName();
        $email    = $response->getEmail();
        $avatar   = $response->getProfilePicture();

        //set data in session
        $this->session->set('nickname', $nickname);
        $this->session->set('realname', $realname);
        $this->session->set('email', $email);
        $this->session->set('avatar', $avatar);

        //get user by fid
        $qb = $this->doctrine->getManager()->createQueryBuilder();
        $qb ->select('u.id')
            ->from('AcmeDemoBundle:User', 'u')
            ->where('u.fid = :fid')
            ->setParameter('fid', $facebook_id)
            ->setMaxResults(1);
        $result = $qb->getQuery()->getResult();

        //add to database if doesn't exists
        if ( !count($result) ) {
            $User = new User();
            $User->setCreatedAt(new \DateTime());
            $User->setNickname($nickname);
            $User->setRealname($realname);
            $User->setEmail($email);
            $User->setAvatar($avatar);
            $User->setFID($facebook_id);

            $em = $this->doctrine->getManager();
            $em->persist($User);
            $id = $em->flush();
        } else {
            $id = $result[0]['id'];
        }

        //set id
        $this->session->set('id', $id);

        //@TODO: hmm : is admin
        if ($this->isUserAdmin($nickname)) {
            $this->session->set('is_admin', true);
        }

        //parent:: returned value
        return $this->loadUserByUsername($response->getNickname());
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\\DemoBundle\\Provider\\OAuthUser';
    }
}


#Acme\DemoBundle\Provider\OAuthUser
<?php

namespace Acme\DemoBundle\Provider;

use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser as HWIOAuthUser;

class OAuthUser extends HWIOAuthUser{
    private $isAdmin = false;
    public function __construct($username, $isAdmin = false)
    {
        parent::__construct($username);
        $this->isAdmin = $isAdmin;
    }

    public function getRoles()
    {
        $roles = array('ROLE_USER', 'ROLE_OAUTH_USER');

        if ($this->isAdmin) {
            array_push($roles, 'ROLE_SUPER_ADMIN');
        }

        return $roles;
    }
}

So you can pretty much see that when facebook login response comes, I do the database checks (based on the facebook graph ID), and add it if needed. Also, I set some things in sessions (you won't need this), and after that I must also return a HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser object (this is where the sf2 gets it's roles). So I extend it (in this way i have acces to $isAdmin). As you said, you need roles for each user and you must also edit them. For that, you can implement getRoles() with a ManyToMany relationship (give access to doctrine entityManager via constructor). You can see that applied here: http://symfony.com/doc/current/cookbook/security/entity_provider.html#managing-roles-in-the-database .

As I said, you must tweak it a lot (my only app is facebook-only login and with a in_memory like security access), but I wish myself I had a code like that when i started. So I really hope this hels you. Post your questions, if any.