Skip to content

Commit

Permalink
feature #18952 [Security] Add a JSON authentication listener (dunglas)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 3.3-dev branch (closes #18952).

Discussion
----------

[Security] Add a JSON authentication listener

| Q | A |
| --- | --- |
| Branch? | master |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | no |
| Deprecations? | no |
| Tests pass? | yes |
| Fixed tickets | n/a |
| License | MIT |
| Doc PR | symfony/symfony-docs#7081 |

Add a new authentication listener allowing to login by sending a JSON document like:

 `{"_username": "dunglas", "_password": "foo"}`.

It is similar to the traditional form login (but take a JSON document as entry) and is convenient for APIs, especially used in combination with JWT.

See api-platform/core#563 and lexik/LexikJWTAuthenticationBundle#123 (comment) for previous discussions.
- [x] Add functional tests in security bundle

Commits
-------

02178bc [Security] Add a JSON authentication listener
  • Loading branch information
fabpot committed Dec 3, 2016
2 parents e8553a8 + 02178bc commit d6e8937
Show file tree
Hide file tree
Showing 12 changed files with 531 additions and 2 deletions.
@@ -0,0 +1,96 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;

/**
* JsonLoginFactory creates services for JSON login authentication.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class JsonLoginFactory extends AbstractFactory
{
public function __construct()
{
$this->addOption('username_path', 'username');
$this->addOption('password_path', 'password');
}

/**
* {@inheritdoc}
*/
public function getPosition()
{
return 'form';
}

/**
* {@inheritdoc}
*/
public function getKey()
{
return 'json-login';
}

/**
* {@inheritdoc}
*/
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'security.authentication.provider.dao.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.dao'))
->replaceArgument(0, new Reference($userProviderId))
->replaceArgument(1, new Reference('security.user_checker.'.$id))
->replaceArgument(2, $id)
;

return $provider;
}

/**
* {@inheritdoc}
*/
protected function getListenerId()
{
return 'security.authentication.listener.json';
}

/**
* {@inheritdoc}
*/
protected function isRememberMeAware($config)
{
return false;
}

/**
* {@inheritdoc}
*/
protected function createListener($container, $id, $config, $userProvider)
{
$listenerId = $this->getListenerId();
$listener = new DefinitionDecorator($listenerId);
$listener->replaceArgument(2, $id);
$listener->replaceArgument(3, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config)));
$listener->replaceArgument(4, new Reference($this->createAuthenticationFailureHandler($container, $id, $config)));
$listener->replaceArgument(5, array_intersect_key($config, $this->options));

$listenerId .= '.'.$id;
$container->setDefinition($listenerId, $listener);

return $listenerId;
}
}
Expand Up @@ -140,7 +140,20 @@
<argument /> <!-- x509 user -->
<argument /> <!-- x509 credentials -->
<argument type="service" id="logger" on-invalid="null" />
<argument type="service" id="event_dispatcher" on-invalid="null"/>
<argument type="service" id="event_dispatcher" on-invalid="null" />
</service>

<service id="security.authentication.listener.json" class="Symfony\Component\Security\Http\Firewall\UsernamePasswordJsonAuthenticationListener" public="false" abstract="true">
<tag name="monolog.logger" channel="security" />
<argument type="service" id="security.token_storage" />
<argument type="service" id="security.authentication.manager" />
<argument /> <!-- Provider-shared Key -->
<argument type="service" id="security.authentication.success_handler" />
<argument type="service" id="security.authentication.failure_handler" />
<argument type="collection" /> <!-- Options -->
<argument type="service" id="logger" on-invalid="null" />
<argument type="service" id="event_dispatcher" on-invalid="null" />
<argument type="service" id="property_accessor" on-invalid="null" />
</service>

<service id="security.authentication.listener.remote_user" class="Symfony\Component\Security\Http\Firewall\RemoteUserAuthenticationListener" public="false" abstract="true">
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/SecurityBundle.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Bundle\SecurityBundle;

use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function build(ContainerBuilder $container)
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new FormLoginFactory());
$extension->addSecurityListenerFactory(new FormLoginLdapFactory());
$extension->addSecurityListenerFactory(new JsonLoginFactory());
$extension->addSecurityListenerFactory(new HttpBasicFactory());
$extension->addSecurityListenerFactory(new HttpBasicLdapFactory());
$extension->addSecurityListenerFactory(new HttpDigestFactory());
Expand Down
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\Controller;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class TestController
{
public function loginCheckAction()
{
throw new \RuntimeException(sprintf('%s should never be called.', __FUNCTION__));
}
}
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class JsonLoginBundle extends Bundle
{
}
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Tests\Functional;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class JsonLoginTest extends WebTestCase
{
public function testJsonLoginSuccess()
{
$client = $this->createClient(array('test_case' => 'JsonLogin', 'root_config' => 'config.yml'));
$client->request('POST', '/chk', array(), array(), array(), '{"user": {"login": "dunglas", "password": "foo"}}');
$this->assertEquals('http://localhost/', $client->getResponse()->headers->get('location'));
}

public function testJsonLoginFailure()
{
$client = $this->createClient(array('test_case' => 'JsonLogin', 'root_config' => 'config.yml'));
$client->request('POST', '/chk', array(), array(), array(), '{"user": {"login": "dunglas", "password": "bad"}}');
$this->assertEquals('http://localhost/login', $client->getResponse()->headers->get('location'));
}
}
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

return array(
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(),
);
@@ -0,0 +1,24 @@
imports:
- { resource: ./../config/framework.yml }

security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext

providers:
in_memory:
memory:
users:
dunglas: { password: foo, roles: [ROLE_USER] }

firewalls:
main:
pattern: ^/
anonymous: true
json_login:
check_path: /mychk
username_path: user.login
password_path: user.password

access_control:
- { path: ^/foo, roles: ROLE_USER }
@@ -0,0 +1,3 @@
login_check:
path: /chk
defaults: { _controller: JsonLoginBundle:Test:loginCheck }
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/SecurityBundle/composer.json
Expand Up @@ -17,7 +17,7 @@
],
"require": {
"php": ">=5.5.9",
"symfony/security": "~3.2",
"symfony/security": "~3.3",
"symfony/http-kernel": "~3.2",
"symfony/polyfill-php70": "~1.0"
},
Expand Down

0 comments on commit d6e8937

Please sign in to comment.