reference implementation of Sylius user login/registration by OAuth sign-in provider account (incomplete)

added step to initially grant access for app in OAuth login scenarios
disabled cache clearing
removed obsolete code
restored cache clearing
added given step to OAuth login scenarios
set placeholder values for resource owner client id + secret
execute scenarios with saucelabs on travic ci apache
added phpspec for oauth user provider
used local selenium server for travis ci instead of sauce labs
This commit is contained in:
headrevision 2013-08-13 16:46:27 +02:00 committed by Joseph Bielawski
parent 8f90c97dc6
commit bc49f46cfe
21 changed files with 536 additions and 24 deletions

View file

@ -66,6 +66,7 @@ class AppKernel extends Kernel
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Knp\Bundle\GaufretteBundle\KnpGaufretteBundle(),
new JMS\SerializerBundle\JMSSerializerBundle($this),
new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
new FOS\RestBundle\FOSRestBundle(),
new FOS\UserBundle\FOSUserBundle(),
new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),

View file

@ -5,6 +5,8 @@
<h1>Registration <small>Create an account in store</small></h1>
</div>
{{ render url('hwi_oauth_connect') }}
<form action="{{ path('fos_user_registration_register') }}" method="post" novalidate class="form-horizontal">
<fieldset class="well">
{% for field in form %}

View file

@ -5,17 +5,19 @@
<h1>Login <small>Sylius store</small></h1>
</div>
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
{% render url('hwi_oauth_connect') %}
<blockquote>
<p>Please login using "<strong>sylius@example.com</strong>" email and "<strong>sylius</strong>" as password.</p>
<small>Mr. Sylius Demo Guide</small>
</blockquote>
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<form action="{{ path('fos_user_security_check') }}" method="post">
<fieldset>
<div class="form-group">

View file

@ -0,0 +1,5 @@
<div class="hero-unit">
{% for owner in hwi_oauth_resource_owners() %}
<a class="oauth-login oauth-login-{{ owner | trans({}, 'HWIOAuthBundle') }}" href="{{ hwi_oauth_login_url(owner) }}" title="{{ owner | trans({}, 'HWIOAuthBundle') }}"><span></span></a>
{% endfor %}
</div>

View file

@ -71,6 +71,19 @@ fos_user:
form:
type: sylius_user_registration
hwi_oauth:
firewall_name: main
resource_owners:
amazon:
type: amazon
client_id: %sylius.oauth.amazon.clientid%
client_secret: %sylius.oauth.amazon.clientsecret%
google:
type: google
client_id: %sylius.oauth.google.clientid%
client_secret: %sylius.oauth.google.clientsecret%
scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
swiftmailer:
transport: %sylius.mailer.transport%
host: %sylius.mailer.host%
@ -130,4 +143,4 @@ knp_snappy:
image:
enabled: true
binary: /usr/bin/wkhtmltoimage
options: []
options: []

View file

@ -1,20 +1,20 @@
parameters:
sylius.database.driver: pdo_mysql
sylius.database.host: 127.0.0.1
sylius.database.port: ~
sylius.database.name: sylius
sylius.database.user: root
sylius.database.password: ~
sylius.database.driver: pdo_mysql
sylius.database.host: 127.0.0.1
sylius.database.port: ~
sylius.database.name: sylius
sylius.database.user: root
sylius.database.password: ~
sylius.mailer.transport: smtp
sylius.mailer.host: 127.0.0.1
sylius.mailer.user: ~
sylius.mailer.password: ~
sylius.mailer.transport: smtp
sylius.mailer.host: 127.0.0.1
sylius.mailer.user: ~
sylius.mailer.password: ~
sylius.locale: en
sylius.secret: abc
sylius.locale: en
sylius.secret: abc
sylius.currency: EUR
sylius.currency: EUR
sylius.cache: file_system
@ -24,4 +24,9 @@ parameters:
paypal.express_checkout.sandbox: true
stripe.secret_key: EDITME
stripe.test_mode: true
stripe.test_mode: true
sylius.oauth.amazon.clientid: <amazon_client_id>
sylius.oauth.amazon.clientsecret: <amazon_client_secret>
sylius.oauth.google.clientid: <google_client_id>
sylius.oauth.google.clientsecret: <google_client_secret>

View file

@ -25,3 +25,17 @@ fos_user_resetting:
fos_user_change_password:
resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
prefix: /account
hwi_oauth_security:
resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
prefix: /connect
hwi_oauth_redirect:
resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
prefix: /login
amazon_login:
pattern: /login/check-amazon
google_login:
pattern: /login/check-google

View file

@ -43,14 +43,24 @@ security:
lifetime: 31536000
always_remember_me: true
remember_me_parameter: _remember_me
oauth:
resource_owners:
amazon: "/login/check-amazon"
google: "/login/check-google"
login_path: /connect
failure_path: /connect
oauth_user_provider:
service: sylius.oauth.user_provider
logout: true
anonymous: true
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/connect.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }

View file

@ -26,6 +26,7 @@
"doctrine/orm": "~2.3",
"friendsofsymfony/rest-bundle": "~1.0",
"friendsofsymfony/user-bundle": "2.0.*@dev",
"hwi/oauth-bundle": "0.3.*@dev",
"incenteev/composer-parameter-handler": "~2.0",
"jms/serializer-bundle": "0.12.*",
"jms/translation-bundle": "1.1.*",

View file

@ -0,0 +1,79 @@
@users
Feature: Sign in to the store via OAuth
In order to view my orders list
As a visitor with an OAuth account
I need to be able to log in to the store
Background:
Given I am not logged in
And I am on the store homepage
Scenario Outline: Get to the OAuth login page
When I follow "Login"
And I press the login with <provider_name> button
Then I should be on the <provider_site> website
And I should see the <provider_name> login form
Examples:
| provider_name | provider_site |
| Amazon | amazon.com |
| Google | google.com |
Scenario Outline: Log in with username and password
Given I follow "Login"
And I press the login with <provider_name> button
When I fill in the following:
| <email_label> | <email_value> |
| <password_label> | <password_value> |
And I press "<button>"
And I allow the use of my <provider_name> account (if I am still on the <provider_site> website)
Then I should not be on the <provider_site> website anymore
But I should be on the store homepage
And I should see "Logout"
Examples:
| provider_name | provider_site | email_label | email_value | password_label | password_value | button |
| Amazon | amazon.com | What is your e-mail address? | a_valid_amazon_email_here | What is your password? | a_valid_amazon_password_here | Sign in using our secure server |
| Google | google.com | Email | a_valid_google_email_here | Password | a_valid_google_password_here | Sign in |
Scenario Outline: Log in with bad credentials
Given I follow "Login"
And I press the login with <provider_name> button
When I fill in the following:
| <email_label> | <email_value> |
| <password_label> | <password_value> |
And I press "<button>"
Then I should still be on the <provider_site> website
And I should see "<error>"
Examples:
| provider_name | provider_site | email_label | email_value | password_label | password_value | button | error |
| Amazon | amazon.com | What is your e-mail address? | a_valid_amazon_email_here | What is your password? | an_invalid_amazon_password_here | Sign in using our secure server | There was an error with your E-Mail/Password combination. Please try again. |
| Google | google.com | Email | a_valid_google_email_here | Password | an_invalid_google_password_here | Sign in | The username or password you entered is incorrect. |
Scenario Outline: Trying to login without credentials
Given I follow "Login"
And I press the login with <provider_name> button
When I press "<button>"
Then I should still be on the <provider_site> website
And I should see "<error>"
Examples:
| provider_name | provider_site | button | error |
| Amazon | amazon.com | Sign in using our secure server | Missing e-mail address. Please correct and try again. |
| Google | google.com | Sign in | Enter your email address. |
Scenario Outline: Trying to login as non existing user
Given I follow "Login"
And I press the login with <provider_name> button
When I fill in the following:
| <email_label> | <email_value> |
| <password_label> | <password_value> |
And I press "<button>"
Then I should still be on the <provider_site> website
And I should see "<error>"
Examples:
| provider_name | provider_site | email_label | email_value | password_label | password_value | button | error |
| Amazon | amazon.com | What is your e-mail address? | an_invalid_amazon_email_address_here | What is your password? | a_valid_amazon_password_here | Sign in using our secure server | There was an error with your E-Mail/Password combination. Please try again. |
| Google | google.com | Email | an_invalid_google_email_address_here | Password | a_valid_google_password_here | Sign in | The username or password you entered is incorrect. |

View file

@ -0,0 +1,56 @@
@users
Feature: User registration via OAuth
In order to order products
As a visitor with an OAuth account
I need to be able to create an account in the store
Background:
Given I am not logged in
And I am on the store homepage
Scenario Outline: Get to the OAuth login page
When I follow "Register"
And I press the login with <provider_name> button
Then I should be on the <provider_site> website
And I should see the <provider_name> login form
Examples:
| provider_name | provider_site |
| Amazon | amazon.com |
| Google | google.com |
Scenario Outline: Successfully creating account in store
Given I follow "Register"
And I press the login with <provider_name> button
When I fill in the following:
| <email_label> | <email_value> |
| <password_label> | <password_value> |
And I press "<button>"
And I allow the use of my <provider_name> account (if I am still on the <provider_site> website)
Then I should not be on the <provider_site> website anymore
But I should be on the store homepage
And I should see "Logout"
Examples:
| provider_name | provider_site | email_label | email_value | password_label | password_value | button |
| Amazon | amazon.com | What is your e-mail address? | a_valid_amazon_email_here | What is your password? | a_valid_amazon_password_here | Sign in using our secure server |
| Google | google.com | Email | a_valid_google_email_here | Password | a_valid_google_password_here | Sign in |
Scenario Outline: Register with already existing email
Given there are following users:
| email |
| <email_value> |
And I follow "Register"
And I press the login with <provider_name> button
When I fill in the following:
| <email_label> | <email_value> |
| <password_label> | <password_value> |
And I press "<button>"
Then I should not be on the <provider_site> website anymore
But I should be on the store homepage
And I should see "Logout"
Examples:
| provider_name | provider_site | email_label | email_value | password_label | password_value | button |
| Amazon | amazon.com | What is your e-mail address? | a_valid_amazon_email_address_here | What is your password? | a_valid_amazon_password_here | Sign in using our secure server |
| Google | google.com | Email | a_valid_google_email_address_here | Password | a_valid_google_password_here | Sign in |

View file

@ -22,6 +22,8 @@ use Sylius\Bundle\AddressingBundle\Model\AddressInterface;
*/
class User extends BaseUser implements UserInterface
{
protected $amazonId;
protected $googleId;
protected $firstName;
protected $lastName;
protected $createdAt;
@ -51,6 +53,52 @@ class User extends BaseUser implements UserInterface
$this->currency = $currency;
}
/**
* Set ID of Amazon account attached to the user
*
* @param string $amazonId
* @return User
*/
public function setAmazonId($amazonId)
{
$this->amazonId = $amazonId;
return $this;
}
/**
* Get ID of Amazon account attached to the user
*
* @return string $amazonId
*/
public function getAmazonId()
{
return $this->amazonId;
}
/**
* Set ID of Google account attached to the user
*
* @param string $googleId
* @return User
*/
public function setGoogleId($googleId)
{
$this->googleId = $googleId;
return $this;
}
/**
* Get ID of Google account attached to the user
*
* @return string $googleId
*/
public function getGoogleId()
{
return $this->googleId;
}
/**
* Get orders
*

View file

@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Bundle\CoreBundle\OAuth;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider;
use Sylius\Bundle\CoreBundle\Model\User;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
/**
* Loading and ad-hoc creation of a user by an OAuth sign-in provider account.
*
* @author Fabian Kiss <fabian.kiss@ymc.ch>
*/
class UserProvider extends FOSUBUserProvider
{
/**
* {@inheritDoc}
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
try {
return parent::loadUserByOAuthUserResponse($response);
} catch (UsernameNotFoundException $e) {
if (null === $user = $this->userManager->findUserByEmail($response->getEmail())) {
return $this->createUserByOAuthUserResponse($response);
} else {
return $this->updateUserByOAuthUserResponse($user, $response);
}
}
}
/**
* {@inheritDoc}
*/
public function connect($user, UserResponseInterface $response)
{
$providerName = $response->getResourceOwner()->getName();
$uniqueId = $response->getUsername();
$user->addOAuthAccount($providerName, $uniqueId);
$this->userManager->updateUser($user);
}
/**
* Ad-hoc creation of user
*
* @param UserResponseInterface $response
*
* @return User
*/
protected function createUserByOAuthUserResponse(UserResponseInterface $response)
{
$user = $this->userManager->createUser();
$this->updateUserByOAuthUserResponse($user, $response);
// set default values taken from OAuth sign-in provider account
if (null !== $email = $response->getEmail()) {
$user->setEmail($email);
}
if (null === $this->userManager->findUserByUsername($response->getNickname())) {
$user->setUsername($response->getNickname());
}
$user->setEnabled(true);
return $user;
}
/**
* Attach OAuth sign-in provider account to existing user
*
* @param User $user
* @param UserResponseInterface $response
*
* @return User
*/
protected function updateUserByOAuthUserResponse(User $user, UserResponseInterface $response)
{
$providerName = $response->getResourceOwner()->getName();
$providerNameSetter = 'set'.ucfirst($providerName).'Id';
$user->$providerNameSetter($response->getUsername());
return $user;
}
}

View file

@ -26,6 +26,9 @@
<field name="firstName" column="first_name" type="string" nullable="true" />
<field name="lastName" column="last_name" type="string" nullable="true" />
<field name="amazonId" column="amazon_id" type="string" nullable="true" />
<field name="googleId" column="google_id" type="string" nullable="true" />
<one-to-many field="orders" target-entity="Sylius\Bundle\OrderBundle\Model\OrderInterface" mapped-by="user">
<cascade>
<cascade-all />

View file

@ -54,6 +54,8 @@
<parameter key="sylius.promotion_action.percentage_discount.class">Sylius\Bundle\CoreBundle\Promotion\Action\PercentageDiscountAction</parameter>
<parameter key="sylius.listener.locale.class">Sylius\Bundle\CoreBundle\EventListener\LocaleListener</parameter>
<parameter key="sylius.oauth.user_provider.class">Sylius\Bundle\CoreBundle\OAuth\UserProvider</parameter>
</parameters>
<services>
@ -262,6 +264,14 @@
<argument type="service" id="sylius.settings.manager" />
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
</service>
<service id="sylius.oauth.user_provider" class="%sylius.oauth.user_provider.class%">
<argument type="service" id="fos_user.user_manager" />
<argument type="collection">
<argument key="amazon">amazonId</argument>
<argument key="google">googleId</argument>
</argument>
</service>
</services>
</container>

View file

@ -0,0 +1,27 @@
<?php
namespace spec\Sylius\Bundle\CoreBundle\OAuth;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class UserProviderSpec extends ObjectBehavior
{
/**
* @param FOS\UserBundle\Model\UserManagerInterface $userManagerInterface
*/
function let($userManagerInterface)
{
$this->beConstructedWith($userManagerInterface, array());
}
function it_is_initializable()
{
$this->shouldHaveType('Sylius\Bundle\CoreBundle\OAuth\UserProvider');
}
function it_implements_Hwi_oauth_aware_user_provider_interface()
{
$this->shouldImplement('HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface');
}
}

View file

@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sylius\Bundle\WebBundle\Behat;
use Behat\Mink\Exception\ExpectationException;
use Behat\MinkExtension\Context\RawMinkContext;
/**
* OAuth context.
*
* @author Fabian Kiss <fabian.kiss@ymc.ch>
*/
class OAuthContext extends RawMinkContext
{
/**
* @Given /^I am not logged in$/
*/
public function iAmNotLoggedIn()
{
$this->getSession('selenium2')->restart();
}
/**
* @When /^I allow the use of my (.+) account \(if I am still on the (.+) website\)$/
*/
public function iAllowTheUseOfMyAccount($providerName, $domain)
{
if ($this->currentUrlContains($domain)) {
$submitButtons = $this->getSession('selenium2')->getPage()->findAll('xpath', '//form//button[@type="submit"]');
if (count($submitButtons) != 2) {
throw new ExpectationException('Page should contain a form with 2 buttons.', $this->getSession('selenium2'));
}
if ($providerName == 'Google') {
$submitButtons[0]->click();
} else {
$submitButtons[1]->click();
}
}
}
/**
* @Then /^I should be on the (.+) website$/
* @Then /^I should still be on the (.+) website$/
*/
public function iShouldBeOnTheWebsite($domain)
{
if (!$this->currentUrlContains($domain)) {
$message = sprintf('Current URL should contain "%s".', $domain);
throw new ExpectationException($message, $this->getSession('selenium2'));
}
}
/**
* @Then /^I should not be on the (.+) website anymore$/
*/
public function iShouldNotBeOnTheWebsiteAnymore($domain)
{
if ($this->currentUrlContains($domain)) {
$message = sprintf('Current URL should not contain "%s".', $domain);
throw new ExpectationException($message, $this->getSession('selenium2'));
}
// Re-set default session
$currentUrl = $this->getSession()->getCurrentUrl();
$this->getMink()->setDefaultSessionName('symfony2');
$this->getSession()->visit($currentUrl);
}
/**
* @Then /^I should see the .+ login form$/
*/
public function iShouldSeeTheLoginForm()
{
$loginForm = $this->getLoginForm();
}
protected function currentUrlContains($domain)
{
$currentUrl = $this->getSession('selenium2')->getCurrentUrl();
return strpos($currentUrl, $domain) !== false;
}
protected function getLoginForm()
{
return $this->assertSession('selenium2')->elementExists('xpath',
'//form//input[@type="email"]' .
'/ancestor::form//input[@type="password"]' .
'/ancestor::form//*[@type="submit"]' .
'/ancestor::form'
);
}
}

View file

@ -53,6 +53,8 @@ class WebUser extends MinkContext implements KernelAwareInterface
{
// Sylius data creation context.
$this->useContext('data', new DataContext());
// Sylius OAuth context.
$this->useContext('oauth', new OAuthContext());
}
/**
@ -733,6 +735,23 @@ class WebUser extends MinkContext implements KernelAwareInterface
}
}
/**
* @When /^I click the login with (.+) button$/
* @When /^I press the login with (.+) button$/
*/
public function iClickTheLoginWithButton($provider)
{
$loginButton = $this->getSession()->getPage()->find('css',
sprintf('a.oauth-login-%s', strtolower($provider))
);
$loginButton->click();
// Re-set default session
$currentUrl = $this->getSession()->getCurrentUrl();
$this->getMink()->setDefaultSessionName('selenium2');
$this->getSession()->visit($currentUrl);
}
/**
* @Given /^I added product "([^""]*)" to cart, with quantity "([^""]*)"$/
* @When /^I add product "([^""]*)" to cart, with quantity "([^""]*)"$/

View file

@ -133,7 +133,6 @@ div.currency-menu {
margin-right: 50px;
}
.checkout-new-customer form, .checkout-existing-customer form {
margin-top: 20px;
}
@ -152,4 +151,22 @@ div.currency-menu {
.checkout-new-customer .form-group > .col-lg-10, .address-container .form-group > .col-lg-10 {
width: 75%;
}
}
a.oauth-login {
margin-right: 8px;
}
a.oauth-login span {
display: inline-block;
width: 64px;
height: 64px;
}
a.oauth-login-amazon span {
background: url('/assets/img/icon/64/amazon.png') no-repeat;
}
a.oauth-login-google span {
background: url('/assets/img/icon/64/google.png') no-repeat;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB