Oct '14

10

Support Form Login and OAuth with Symfony 2

The security component for Symfony 2 can be tough to get a handle on. Customizing your login process beyond the initial form login is tricky, with many options to improve your user's experience. OAuth is one such option, giving users the ability to use their social media accounts. As Wikipedia so eloquently puts:

OAuth is an open standard to authorization. OAuth provides client applications a 'secure delegated access' to server resources on behalf of a resource owner. It specifies a process for resource owners to authorize third-party access to their server resources without sharing their credentials.

This website uses OAuth to facilitate Twitter and GitHub login using HWIOAuthBundle for Symfony 2. It allows a user to login with a username and password as well, though there is currently no way to register an account without OAuth.

This article will walk through the steps required to set up form login and OAuth login using the same User entity.

 

Getting Started

To start out, this guide assumes you have a working form login with entities setup for your Symfony 2 application. We'll be installing and configuring HWIOAuthBundle and creating an Authentication Provider that knows how to handle both form and OAuth requests. We'll also create a User Provider that knows how to lookup an existing User entity or create one when someone logs in from an OAuth provider for the first time.

This article makes use of OrkestraApplicationBundle's User entity, but any other entity used as a UserInterface (described here) will work.

 

Installing HWIOAuthBundle

The easiest way to install this in your project, is from your project's root directory, run composer require hwi/oauth-bundle ~0.3. This will update your composer.json and install the bundle and any dependencies.

Check out HWIOAuthBundle documentation for more details. We will finish configuring the bundle after we have our code written.

 

Building your Authentication Provider

The first thing we need to do is implement an overriding Authentication Provider that knows how to handle OAuth tokens populated by HWIOAuth and our user entities. Later, we will configure Symfony to override appropriately.

Below is an excerpt from the OAuthProvider. This is the main method, authenticate, with some comments highlighting what is going on.

class OAuthProvider implements AuthenticationProviderInterface
{
    public function authenticate(TokenInterface $token)
    {
        $resourceOwner = $this->resourceOwnerMap->getResourceOwnerByName($token->getResourceOwnerName());

        // If the token is not yet expired, attempt to load the user
        $user = null;
        if (!$token->isExpired()) {
            try {
                $user = $this->userProvider->loadUserByUsername($token->getUsername());
            } catch (\Exception $e) {
                // But fail silently if unable to load the user
            }
        }

        // If no user was loaded above, attempt to load a user with 
        // the OAuth response stored in the token
        if (!$user) {
            $userResponse = $resourceOwner->getUserInformation($token->getRawToken());

            try {
                $user = $this->userProvider->loadUserByOAuthUserResponse($userResponse);
            } catch (OAuthAwareExceptionInterface $e) {
                $e->setToken($token);
                $e->setResourceOwnerName($token->getResourceOwnerName());

                throw $e;
            }
        }

        // Create a new, authenticated token with the new or existing user
        $token = new OAuthToken($token->getRawToken(), $user->getRoles());
        $token->setResourceOwnerName($resourceOwner->getName());
        $token->setUser($user);
        $token->setAuthenticated(true);

        $this->userChecker->checkPostAuth($user);

        return $token;
    }
}

View the full class here. To summarize:

  • If the token is not expired, we attempt to load a user
  • If loading the user from an existing token fails or does not occur, we load a user from the OAuth response
  • Finally, we return a brand new, authenticated token

 

Building your User Provider

Now that we can authenticate a token, we need to implement the parts that know how to create and load users. Because this provider will support both regular usernames and passwords as well as tokens, we need to implement a way to load User entities from a username as well as an OAuth response. In addition, we must be able to create brand new users from an OAuth response if one does not yet exist.

class OAuthUserProvider implements OAuthAwareUserProviderInterface, UserProviderInterface
{
    /**
     * Load a user entity by the given username
     */
    public function loadUserByUsername($username)
    {
        $user = $this->repository->loadUserByUsername($username);

        if (null === $user) {
            throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
        }

        return $user;
    }
    
    /**
     * Attempt to load an existing user from an OAuth response
     * Create a user if an existing one is not found
     */
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        try {
            $user = $this->loadUserByUsername(sprintf('%s-%s', $response->getResourceOwner()->getName(), $response->getUsername()));
        } catch (UsernameNotFoundException $e) {
            $user = $this->createUserFromResponse($response);
        }

        return $user;
    }

    /**
     * Creates a new user from an OAuth response
     */
    protected function createUserFromResponse(UserResponseInterface $response)
    {
        if (!$response->getUsername()) {
            throw new \RuntimeException('Unable to authenticate. An error occurred during OAuth authentication.');
        }

        $group = $this->entityManager->getRepository('OrkestraApplicationBundle:Group')->findOneBy(array('role' => 'ROLE_USER'));

        if (!$group) {
            throw new \RuntimeException('Unable to locate user group with role "ROLE_USER".');
        }

        $user = new User();
        $user->setUsername(sprintf('%s-%s', $response->getResourceOwner()->getName(), $response->getUsername()));
        list ($firstName, $lastName) = explode(' ', $response->getRealName(), 2);
        $user->setFirstName((string) $firstName);
        $user->setLastName((string) $lastName);
        $user->setGroups($group);

        $factory = $this->encoderFactory->getEncoder($user);
        $user->setPassword($factory->encodePassword(md5(uniqid(mt_rand(), true)), $user->getSalt()));

        $author = new Author();
        $author->setUser($user);

        $this->entityManager->persist($author);
        $this->entityManager->flush();

        return $user;
    }
}

View the full class here. We now have:

  • A method to load a user with the given username (Same as a regular entity UserProvider)
  • A method to load a user from an OAuth response
  • A method to create a user from an OAuth response

 

 

Configuring your application

Now that the code is written, we need to override the default HWIOAuth Authentication Provider and let Symfony know about our custom user provider. In your application's bundle services.yml, add the following:

parameters:
  hwi_oauth.authentication.provider.oauth.class: MyProject\Bundle\BlogBundle\Security\OAuthProvider

services:
  my_project.oauth.user_provider:
    class: MyProject\Bundle\BlogBundle\Security\OAuthUserProvider
    arguments: [ @doctrine.orm.entity_manager, @security.encoder_factory ]

 

Next, we need to update our security.yml to use the custom user provider and configure OAuth:

security:
  providers:
    oauth:
      id: my_project.oauth.user_provider

  firewalls:
    user_area:
      anonymous: true
      logout: ~
      form_login:
        login_path: /connect
        check_path: /login/check
        username_parameter: "form[username]"
        password_parameter: "form[password]"
      oauth:
        login_path: /connect
        failure_path: /connect
        resource_owners:
          twitter:  "/login/check-twitter"
          github:   "/login/check-github"
        oauth_user_provider:
          service: my_project.oauth.user_provider

 

Then, in your config.yml, add:

hwi_oauth:
  firewall_name: user_area
  resource_owners:
    github:
      type:           github
      client_id:      "github client id"
      client_secret:  "github secret"
    twitter:
      type:           twitter
      client_id:      "twitter client id"
      client_secret:  "twitter secret"

 

Finally, add some new routes to your routing.yml:

hwi_oauth_redirect:
  resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
  prefix:   /connect

# Authentication routes
login:
  pattern: /connect
  defaults: { _controller: MyProjectBlogBundle:Auth:login }

login_check:
  pattern: /login/check

twitter_login:
  pattern: /login/check-twitter

github_login:
  pattern: /login/check-github

logout:
  pattern: /logout

 

This configuration will put your login page at /connect. Regular login forms will post to /login/check, Twitter will post to /login/check-twitter, and GitHub will post to /login/check-github. If you're adding more providers, follow the above configuration steps for each provider.

And we're done! You should now be authenticating in style with any OAuth providers you want!

phpsymfony2doctrine2Programming

Comments

No comments yet! Say something.