Browse Source

Initial setup of the HWIOAuthBundle for GitHub integration/login

Alexander 12 years ago
parent
commit
df3c5b4c54

+ 1 - 0
app/AppKernel.php

@@ -20,6 +20,7 @@ class AppKernel extends Kernel
             new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
             new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
             new FOS\UserBundle\FOSUserBundle(),
+            new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
             new Snc\RedisBundle\SncRedisBundle(),
             new Packagist\WebBundle\PackagistWebBundle(),
             new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),

+ 1 - 1
app/Resources/FOSUserBundle/views/Security/login.html.twig

@@ -5,7 +5,7 @@
     <div>{{ error }}</div>
 {% endif %}
 
-<form action="{{ path("fos_user_security_check") }}" method="post">
+<form action="/login_check" method="post">
     <div>
         <label for="username">{{ 'security.login.username'|trans({}, 'FOSUserBundle') }}</label>
         <input type="text" id="username" name="_username" value="{{ last_username }}" />

+ 13 - 0
app/config/config.yml

@@ -72,6 +72,19 @@ fos_user:
         form:
             handler: packagist.form.handler.registration
 
+hwi_oauth:
+    firewall_name: main
+    connect:
+        account_connector: packagist.user_provider
+        registration_form_handler: packagist.oauth.registration_form_handler
+        registration_form: packagist.oauth.registration_form
+    resource_owners:
+        github:
+            type:          github
+            client_id:     %github.client_id%
+            client_secret: %github.client_secret%
+            scope:         ""
+
 nelmio_solarium:
     adapter: ~
 

+ 5 - 1
app/config/parameters.yml.dist

@@ -20,4 +20,8 @@ parameters:
     remember_me.secret: CHANGE_ME_IN_PROD
 
     google_analytics:
-        ga_key:
+        ga_key:
+
+    github:
+        client_id:
+        client_secret:

+ 26 - 3
app/config/routing.yml

@@ -2,9 +2,6 @@ _packagist:
     resource: "@PackagistWebBundle/Controller"
     type:     annotation
 
-fos_user_security:
-    resource: "@FOSUserBundle/Resources/config/routing/security.xml"
-
 fos_user_profile:
     resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
     prefix: /profile
@@ -19,3 +16,29 @@ fos_user_resetting:
 
 fos_user_change_password:
     resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
+
+
+hwi_oauth_connect:
+    resource: "@HWIOAuthBundle/Resources/config/routing/connect.xml"
+    prefix:   /connect
+
+# overrides the fosub /login page
+hwi_oauth_login:
+    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
+    prefix:   /login
+
+hwi_oauth_redirect:
+    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
+    prefix:   /login
+
+github_login:
+    pattern: /login/github
+
+github_check:
+    pattern: /login/check-github
+
+logout:
+    pattern: /logout
+
+login_check:
+    pattern: /login_check

+ 8 - 1
app/config/security.yml

@@ -26,6 +26,13 @@ security:
                 lifetime: 31104000 # 1y
             logout:       true
             anonymous:    true
+            oauth:
+                resource_owners:
+                    github: "/login/check-github"
+                login_path:        /login
+                failure_path:      /login
+                oauth_user_provider:
+                    service: packagist.user_provider
             switch_user:
                 provider: packagist
 
@@ -52,4 +59,4 @@ security:
         ROLE_EDIT_PACKAGES: ~
 
         ROLE_ADMIN:       [ ROLE_USER, ROLE_UPDATE_PACKAGES, ROLE_EDIT_PACKAGES, ROLE_DELETE_PACKAGES ]
-        ROLE_SUPERADMIN:  [ ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
+        ROLE_SUPERADMIN:  [ ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]

+ 2 - 1
composer.json

@@ -29,6 +29,7 @@
         "doctrine/doctrine-bundle": "dev-master",
         "doctrine/orm": "2.2.*",
         "friendsofsymfony/user-bundle": "dev-master",
+        "hwi/oauth-bundle": "dev-master",
         "nelmio/solarium-bundle": "dev-master",
         "predis/predis": "0.7.*",
         "sensio/distribution-bundle": "dev-master",
@@ -58,4 +59,4 @@
         "symfony-app-dir": "app",
         "symfony-web-dir": "web"
     }
-}
+}

+ 12 - 1
composer.lock

@@ -1,5 +1,5 @@
 {
-    "hash": "7ec0e6e0a9c4da86f2c0093fc032a99a",
+    "hash": "5bc65e1bfee6c7cde7f85f19d7cf2f19",
     "packages": [
         {
             "package": "composer/composer",
@@ -35,6 +35,12 @@
             "source-reference": "7a9d20e69ac0363432fae86523776b14592993d7",
             "commit-date": "1343834547"
         },
+        {
+            "package": "hwi/oauth-bundle",
+            "version": "dev-master",
+            "source-reference": "ad2f468abdfe09e8f9c1398ad4637f51df7d7791",
+            "commit-date": "1342353157"
+        },
         {
             "package": "justinrainbow/json-schema",
             "version": "1.1.0"
@@ -51,6 +57,10 @@
             "source-reference": "d1d5066d9071a76d771993081ee23137e96216b7",
             "commit-date": "1344507381"
         },
+        {
+            "package": "kriswallsmith/buzz",
+            "version": "v0.7"
+        },
         {
             "package": "monolog/monolog",
             "version": "1.1.0"
@@ -201,6 +211,7 @@
         "composer/composer": 20,
         "doctrine/doctrine-bundle": 20,
         "friendsofsymfony/user-bundle": 20,
+        "hwi/oauth-bundle": 20,
         "nelmio/solarium-bundle": 20,
         "sensio/distribution-bundle": 20,
         "sensio/framework-extra-bundle": 20,

+ 27 - 1
src/Packagist/WebBundle/Entity/User.php

@@ -50,6 +50,12 @@ class User extends BaseUser
      */
     private $apiToken;
 
+    /**
+     * @ORM\Column(type="string", length=255, nullable=true)
+     * @var string
+     */
+    private $githubId;
+
     public function __construct()
     {
         $this->packages = new ArrayCollection();
@@ -145,4 +151,24 @@ class User extends BaseUser
     {
         return $this->apiToken;
     }
-}
+
+    /**
+     * Get githubId.
+     *
+     * @return string
+     */
+    public function getGithubId()
+    {
+        return $this->githubId;
+    }
+
+    /**
+     * Set githubId.
+     *
+     * @param string $githubId
+     */
+    public function setGithubId($githubId)
+    {
+        $this->githubId = $githubId;
+    }
+}

+ 97 - 0
src/Packagist/WebBundle/Form/Handler/OAuthRegistrationFormHandler.php

@@ -0,0 +1,97 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Form\Handler;
+
+use FOS\UserBundle\Model\UserManagerInterface;
+use FOS\UserBundle\Util\TokenGeneratorInterface;
+use HWI\Bundle\OAuthBundle\Form\RegistrationFormHandlerInterface;
+use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
+use Symfony\Component\Form\Form;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * OAuthRegistrationFormHandler
+ *
+ * @author Alexander <iam.asm89@gmail.com>
+ */
+class OAuthRegistrationFormHandler implements RegistrationFormHandlerInterface
+{
+    private $userManager;
+    private $tokenGenerator;
+
+    /**
+     * Constructor.
+     *
+     * @param UserManagerInterface $userManager
+     */
+    public function __construct(UserManagerInterface $userManager, TokenGeneratorInterface $tokenGenerator)
+    {
+        $this->tokenGenerator = $tokenGenerator;
+        $this->userManager = $userManager;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    function process(Request $request, Form $form, UserResponseInterface $userInformation)
+    {
+        $user = $this->userManager->createUser();
+
+        $form->setData($user);
+
+        if ('POST' === $request->getMethod()) {
+            $form->bindRequest($request);
+
+            if ($form->isValid()) {
+                $randomPassword = $this->tokenGenerator->generateToken();
+                $form->getData()->setPassword($randomPassword);
+
+                return true;
+            }
+        // if the form is not posted we'll try to set some properties
+        } else {
+            $user = $form->getData();
+
+            $user->setUsername($this->getUniqueUsername($userInformation->getUsername()));
+
+            if ($userInformation instanceof AdvancedUserResponseInterface) {
+                $user->setEmail($userInformation->getEmail());
+            }
+
+            $form->setData($user);
+        }
+
+        return false;
+    }
+
+    /**
+     * Attempts to get a unique username for the user.
+     *
+     * @param string $name
+     *
+     * @return string Name, or empty string if it failed after 10 times
+     *
+     * @see HWI\Bundle\OAuthBundle\Form\FOSUBRegistrationHandler
+     */
+    protected function getUniqueUserName($name)
+    {
+        $i = 0;
+        $testName = $name;
+
+        do {
+            $user = $this->userManager->findUserByUsername($testName);
+        } while ($user !== null && $i < 10 && $testName = $name.++$i);
+
+        return $user !== null ? '' : $testName;
+    }
+}

+ 41 - 0
src/Packagist/WebBundle/Form/Type/OAuthRegistrationFormType.php

@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Form\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class OAuthRegistrationFormType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options)
+    {
+        $builder
+            ->add('username', null, array('label' => 'form.username', 'translation_domain' => 'FOSUserBundle'))
+            ->add('email', 'email', array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle'))
+        ;
+    }
+
+    public function setDefaultOptions(OptionsResolverInterface $resolver)
+    {
+        $resolver->setDefaults(array(
+            'data_class' => 'Packagist\WebBundle\Entity\User',
+            'intention'  => 'registration',
+        ));
+    }
+
+    public function getName()
+    {
+        return 'packagist_oauth_user_registration';
+    }
+}

+ 17 - 0
src/Packagist/WebBundle/Resources/config/services.yml

@@ -32,3 +32,20 @@ services:
     fos_user.util.user_manipulator:
         class: Packagist\WebBundle\Util\UserManipulator
         arguments: [@fos_user.user_manager, @fos_user.util.token_generator]
+
+    packagist.oauth.registration_form_handler:
+        class: Packagist\WebBundle\Form\Handler\OAuthRegistrationFormHandler
+        arguments: [@fos_user.user_manager, @fos_user.util.token_generator]
+
+    packagist.oauth.registration_form_type:
+        class: Packagist\WebBundle\Form\Type\OAuthRegistrationFormType
+        tags:
+            - { name: form.type, alias: packagist_oauth_user_registration }
+
+    packagist.oauth.registration_form:
+        factory_method: createNamed
+        factory_service: form.factory
+        class: Symfony\Component\Form\Form
+        arguments:
+            - 'packagist_oauth_user_registration'
+            - 'packagist_oauth_user_registration'

+ 2 - 2
src/Packagist/WebBundle/Resources/views/Web/index.html.twig

@@ -64,9 +64,9 @@ require 'vendor/autoload.php';
                 <h2>Commit The File</h2>
                 <p>You surely don't need help with that.</p>
                 <h2>Publish It</h2>
-                <p><a href="{{ path('fos_user_security_login') }}">Login</a> or <a href="{{ path('fos_user_registration_register') }}">register</a> on this site, then hit the big fat green button above that says <a href="{{ path('submit') }}">submit</a>.</p>
+                <p><a href="{{ path('hwi_oauth_connect') }}">Login</a> or <a href="{{ path('fos_user_registration_register') }}">register</a> on this site, then hit the big fat green button above that says <a href="{{ path('submit') }}">submit</a>.</p>
                 <p>Once you entered your public repository URL in there, your package will be automatically crawled periodically. You just have to make sure you keep the composer.json file up to date.</p>
             </div>
         </section>
     </div>
-{% endblock %}
+{% endblock %}

+ 4 - 4
src/Packagist/WebBundle/Resources/views/layout.html.twig

@@ -33,11 +33,11 @@
         <div class="container">
             <div class="user">
                 {% if app.user %}
-                    <a href="{{ path('fos_user_profile_show') }}">{{ app.user.username }}</a> | <a href="{{ path('fos_user_security_logout') }}">Logout</a>
+                    <a href="{{ path('fos_user_profile_show') }}">{{ app.user.username }}</a> | <a href="/logout">Logout</a>
                 {% else %}
                     <a href="{{ path('fos_user_registration_register') }}">Create a new account</a>
                     |
-                    <a href="{{ path('fos_user_security_login') }}">Login</a>
+                    <a href="{{ path('hwi_oauth_connect') }}">Login</a>
                 {% endif %}
             </div>
 
@@ -81,10 +81,10 @@
             <ul>
                 {% if app.user %}
                     <li><a href="{{ path('fos_user_profile_show') }}">{{ 'menu.profile'|trans }}</a></li>
-                    <li><a href="{{ path('fos_user_security_logout') }}">{{ 'menu.logout'|trans }}</a></li>
+                    <li><a href="/logout">{{ 'menu.logout'|trans }}</a></li>
                 {% else %}
                     <li><a href="{{ path('fos_user_registration_register') }}">{{ 'menu.register'|trans }}</a></li>
-                    <li><a href="{{ path('fos_user_security_login') }}">{{ 'menu.login'|trans }}</a></li>
+                    <li><a href="{{ path('hwi_oauth_connect') }}">{{ 'menu.login'|trans }}</a></li>
                 {% endif %}
             </ul>
             <ul>

+ 38 - 1
src/Packagist/WebBundle/Security/Provider/UserProvider.php

@@ -13,11 +13,15 @@
 namespace Packagist\WebBundle\Security\Provider;
 
 use FOS\UserBundle\Model\UserManagerInterface;
+use HWI\Bundle\OAuthBundle\Connect\AccountConnectorInterface;
+use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
+use HWI\Bundle\OAuthBundle\Security\Core\Exception\AccountNotLinkedException;
+use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
 use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
 use Symfony\Component\Security\Core\User\UserProviderInterface;
 use Symfony\Component\Security\Core\User\UserInterface;
 
-class UserProvider implements UserProviderInterface
+class UserProvider implements OAuthAwareUserProviderInterface, UserProviderInterface
 {
     /**
      * @var UserManagerInterface
@@ -32,6 +36,39 @@ class UserProvider implements UserProviderInterface
         $this->userManager = $userManager;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public function connect($user, UserResponseInterface $response)
+    {
+        $username = $response->getUsername();
+
+        // 'disconnect' a previous account
+        if (null !== $previousUser = $this->userManager->findUserBy(array('githubId' => $username))) {
+            $previousUser->setGithubId(null);
+            $this->userManager->updateUser($previousUser);
+        }
+
+        $user->setGithubId($username);
+
+        $this->userManager->updateUser($user);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
+    {
+        $username = $response->getUsername();
+        $user = $this->userManager->findUserBy(array('githubId' => $username));
+
+        if (!$user) {
+            throw new AccountNotLinkedException(sprintf('No user with github username "%s" was found.', $username));
+        }
+
+        return $user;
+    }
+
     /**
      * {@inheritDoc}
      */