From c3235553ddc2bb5965f6fe00e750cfe4aac9ccdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Sat, 31 Jan 2015 15:14:10 +0100 Subject: [PATCH] first implementation of security --- app/config/routing.yml | 8 ++ app/config/security.yml | 80 +++++++++-------- .../Controller/SecurityController.php | 27 ++++++ .../Controller/WallabagRestController.php | 9 +- .../Security/Factory/WsseFactory.php | 40 +++++++++ src/Wallabag/CoreBundle/Entity/Entries.php | 1 + src/Wallabag/CoreBundle/Entity/Users.php | 87 ++++++++++++++++++- src/Wallabag/CoreBundle/Helper/Entries.php | 10 +++ .../Repository/EntriesRepository.php | 3 + .../CoreBundle/Resources/config/services.xml | 16 +++- .../Resources/views/Security/login.html.twig | 32 +++++++ .../Resources/views/_menu.html.twig | 2 +- .../Resources/views/layout-login.html.twig | 26 ++++++ .../Resources/views/layout.html.twig | 48 +++++----- .../Authentication/Provider/WsseProvider.php | 59 +++++++++++++ .../Authentication/Token/WsseUserToken.php | 23 +++++ .../Security/Firewall/WsseListener.php | 58 +++++++++++++ .../CoreBundle/WallabagCoreBundle.php | 9 ++ 18 files changed, 469 insertions(+), 69 deletions(-) create mode 100644 src/Wallabag/CoreBundle/Controller/SecurityController.php create mode 100644 src/Wallabag/CoreBundle/DependencyInjection/Security/Factory/WsseFactory.php create mode 100644 src/Wallabag/CoreBundle/Helper/Entries.php create mode 100644 src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig create mode 100644 src/Wallabag/CoreBundle/Resources/views/layout-login.html.twig create mode 100644 src/Wallabag/CoreBundle/Security/Authentication/Provider/WsseProvider.php create mode 100644 src/Wallabag/CoreBundle/Security/Authentication/Token/WsseUserToken.php create mode 100644 src/Wallabag/CoreBundle/Security/Firewall/WsseListener.php diff --git a/app/config/routing.yml b/app/config/routing.yml index 8e04a0c81..426dcdcfe 100644 --- a/app/config/routing.yml +++ b/app/config/routing.yml @@ -10,6 +10,14 @@ doc-api: resource: "@NelmioApiDocBundle/Resources/config/routing.yml" prefix: /api/doc +login: + pattern: /login + defaults: { _controller: WallabagCoreBundle:Security:login } +login_check: + pattern: /login_check +logout: + path: /logout + #wallabag_api: # resource: "@WallabagApiBundle/Controller/" # type: annotation diff --git a/app/config/security.yml b/app/config/security.yml index a28b1db99..f4fefe2e4 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -1,52 +1,58 @@ -# you can read more about security in the related section of the documentation -# http://symfony.com/doc/current/book/security.html security: - # http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password encoders: - Symfony\Component\Security\Core\User\User: plaintext + Wallabag\CoreBundle\Entity\Users: + algorithm: sha1 + encode_as_base64: false + iterations: 1 - # http://symfony.com/doc/current/book/security.html#hierarchical-roles role_hierarchy: ROLE_ADMIN: ROLE_USER - ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] + ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ] - # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers providers: - in_memory: - memory: - users: - user: { password: userpass, roles: [ 'ROLE_USER' ] } - admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } + administrators: + entity: { class: WallabagCoreBundle:Users, property: username } # the main part of the security, where you can set up firewalls # for specific sections of your app firewalls: - # disables authentication for assets and the profiler, adapt it according to your needs - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - # the login page has to be accessible for everybody - demo_login: - pattern: ^/demo/secured/login$ - security: false + #wsse_secured: + # pattern: /api/.* + # wsse: true + login_firewall: + pattern: ^/login$ + anonymous: ~ - # secures part of the application - demo_secured_area: - pattern: ^/demo/secured/ - # it's important to notice that in this case _demo_security_check and _demo_login - # are route names and that they are specified in the AcmeDemoBundle + secured_area: + pattern: ^/ + anonymous: ~ form_login: - check_path: _demo_security_check - login_path: _demo_login - logout: - path: _demo_logout - target: _demo - #anonymous: ~ - #http_basic: - # realm: "Secured Demo Area" + login_path: /login + + use_forward: false + + check_path: /login_check + + post_only: true + + always_use_default_target_path: true + default_target_path: / + target_path_parameter: redirect_url + use_referer: true + + failure_path: null + failure_forward: false + + username_parameter: _username + password_parameter: _password + + csrf_parameter: _csrf_token + intention: authenticate + + logout: + path: /logout + target: / - # with these settings you can restrict or allow access for different parts - # of your application based on roles, ip, host or methods - # http://symfony.com/doc/current/cookbook/security/access_control.html access_control: - #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } \ No newline at end of file + - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/, roles: ROLE_USER } diff --git a/src/Wallabag/CoreBundle/Controller/SecurityController.php b/src/Wallabag/CoreBundle/Controller/SecurityController.php new file mode 100644 index 000000000..51f9cc265 --- /dev/null +++ b/src/Wallabag/CoreBundle/Controller/SecurityController.php @@ -0,0 +1,27 @@ +getSession(); + // get the login error if there is one + if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { + $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR); + } else { + $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); + $session->remove(SecurityContext::AUTHENTICATION_ERROR); + } + return $this->render('WallabagCoreBundle:Security:login.html.twig', array( + // last username entered by the user + 'last_username' => $session->get(SecurityContext::LAST_USERNAME), + 'error' => $error, + )); + } +} \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/Controller/WallabagRestController.php b/src/Wallabag/CoreBundle/Controller/WallabagRestController.php index a6c0db37a..8e018e88b 100644 --- a/src/Wallabag/CoreBundle/Controller/WallabagRestController.php +++ b/src/Wallabag/CoreBundle/Controller/WallabagRestController.php @@ -82,17 +82,18 @@ class WallabagRestController extends Controller */ public function postEntriesAction(Request $request) { - //TODO la récup ne marche + //TODO la récup ne marche pas //TODO gérer si on passe le titre //TODO gérer si on passe les tags //TODO ne pas avoir du code comme ça qui doit se trouver dans le Repository + $url = $request->request->get('url'); + + $content = Extractor::extract($url); $entry = new Entries(); $entry->setUserId(1); - $content = Extractor::extract($request->request->get('url')); - + $entry->setUrl($url); $entry->setTitle($content->getTitle()); $entry->setContent($content->getBody()); - $em = $this->getDoctrine()->getManager(); $em->persist($entry); $em->flush(); diff --git a/src/Wallabag/CoreBundle/DependencyInjection/Security/Factory/WsseFactory.php b/src/Wallabag/CoreBundle/DependencyInjection/Security/Factory/WsseFactory.php new file mode 100644 index 000000000..9807fe9af --- /dev/null +++ b/src/Wallabag/CoreBundle/DependencyInjection/Security/Factory/WsseFactory.php @@ -0,0 +1,40 @@ +setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider')) + ->replaceArgument(0, new Reference($userProvider)) + ; + + $listenerId = 'security.authentication.listener.wsse.'.$id; + $listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener')); + + return array($providerId, $listenerId, $defaultEntryPoint); + } + + public function getPosition() + { + return 'pre_auth'; + } + + public function getKey() + { + return 'wsse'; + } + + public function addConfiguration(NodeDefinition $node) + { + } +} \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/Entity/Entries.php b/src/Wallabag/CoreBundle/Entity/Entries.php index 712ff1262..3c061a37b 100644 --- a/src/Wallabag/CoreBundle/Entity/Entries.php +++ b/src/Wallabag/CoreBundle/Entity/Entries.php @@ -10,6 +10,7 @@ use Symfony\Component\Validator\Constraints as Assert; * * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\EntriesRepository") * @ORM\Table(name="entries") + * */ class Entries { diff --git a/src/Wallabag/CoreBundle/Entity/Users.php b/src/Wallabag/CoreBundle/Entity/Users.php index 3db4a3fd9..96867bd61 100644 --- a/src/Wallabag/CoreBundle/Entity/Users.php +++ b/src/Wallabag/CoreBundle/Entity/Users.php @@ -3,6 +3,9 @@ namespace Wallabag\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\EquatableInterface; +use Symfony\Component\Security\Core\User\AdvancedUserInterface; /** * Users @@ -10,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM; * @ORM\Table(name="users") * @ORM\Entity */ -class Users +class Users implements AdvancedUserInterface, \Serializable { /** * @var integer @@ -28,6 +31,11 @@ class Users */ private $username; + /** + * @ORM\Column(type="string", length=32) + */ + private $salt; + /** * @var string * @@ -49,7 +57,16 @@ class Users */ private $email; + /** + * @ORM\Column(name="is_active", type="boolean") + */ + private $isActive; + public function __construct() + { + $this->isActive = true; + $this->salt = md5(uniqid(null, true)); + } /** * Get id @@ -84,6 +101,22 @@ class Users return $this->username; } + /** + * @inheritDoc + */ + public function getSalt() + { + return $this->salt; + } + + /** + * @inheritDoc + */ + public function getRoles() + { + return array('ROLE_USER'); + } + /** * Set password * @@ -152,4 +185,56 @@ class Users { return $this->email; } + + /** + * @inheritDoc + */ + public function eraseCredentials() + { + } + + /** + * @see \Serializable::serialize() + */ + public function serialize() + { + return serialize(array( + $this->id, + )); + } + + /** + * @see \Serializable::unserialize() + */ + public function unserialize($serialized) + { + list ( + $this->id, + ) = unserialize($serialized); + } + + public function isEqualTo(UserInterface $user) + { + return $this->username === $user->getUsername(); + } + + public function isAccountNonExpired() + { + return true; + } + + public function isAccountNonLocked() + { + return true; + } + + public function isCredentialsNonExpired() + { + return true; + } + + public function isEnabled() + { + return $this->isActive; + } } diff --git a/src/Wallabag/CoreBundle/Helper/Entries.php b/src/Wallabag/CoreBundle/Helper/Entries.php new file mode 100644 index 000000000..a54c3a74d --- /dev/null +++ b/src/Wallabag/CoreBundle/Helper/Entries.php @@ -0,0 +1,10 @@ +createQueryBuilder('e') ->select('e') ->where('e.isFav =:isStarred')->setParameter('isStarred', $isStarred) diff --git a/src/Wallabag/CoreBundle/Resources/config/services.xml b/src/Wallabag/CoreBundle/Resources/config/services.xml index 02308e6aa..d5bc5cca5 100644 --- a/src/Wallabag/CoreBundle/Resources/config/services.xml +++ b/src/Wallabag/CoreBundle/Resources/config/services.xml @@ -5,13 +5,25 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + + + %kernel.cache_dir%/security/nonces + + + + + + - - diff --git a/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig b/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig new file mode 100644 index 000000000..2437e3b0d --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/Security/login.html.twig @@ -0,0 +1,32 @@ +{% extends "WallabagCoreBundle::layout-login.html.twig" %} + +{% block title %}{% trans %}login to your wallabag{% endtrans %}{% endblock %} +{% block content %} + {% if error %} +
{{ error.message }}
+ {% endif %} + +
+
+

{% trans %}Login to wallabag{% endtrans %}

+ +
+ + +
+ +
+ + +
+ {# + Si vous voulez contrôler l'URL vers laquelle l'utilisateur est redirigé en cas de succès + (plus de détails ci-dessous) + + #} +
+ +
+
+
+{% endblock %} diff --git a/src/Wallabag/CoreBundle/Resources/views/_menu.html.twig b/src/Wallabag/CoreBundle/Resources/views/_menu.html.twig index d4560e84b..3065ce446 100644 --- a/src/Wallabag/CoreBundle/Resources/views/_menu.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/_menu.html.twig @@ -10,6 +10,6 @@
  • {% trans %}config{% endtrans %}
  • {% trans %}about{% endtrans %}
  • -
  • {% trans %}logout{% endtrans %}
  • +
  • {% trans %}logout{% endtrans %}
  • diff --git a/src/Wallabag/CoreBundle/Resources/views/layout-login.html.twig b/src/Wallabag/CoreBundle/Resources/views/layout-login.html.twig new file mode 100644 index 000000000..9c39ea9f8 --- /dev/null +++ b/src/Wallabag/CoreBundle/Resources/views/layout-login.html.twig @@ -0,0 +1,26 @@ + + + + + + + + + + + {% block title %}{% endblock %} - wallabag + {% include "WallabagCoreBundle::_head.html.twig" %} + + + {% include "WallabagCoreBundle::_top.html.twig" %} +
    + {% block menu %}{% endblock %} +
    + {% block content %}{% endblock %} +
    +
    + {% include "WallabagCoreBundle::_footer.html.twig" %} + + \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/Resources/views/layout.html.twig b/src/Wallabag/CoreBundle/Resources/views/layout.html.twig index 83830a4ae..1f1753a48 100644 --- a/src/Wallabag/CoreBundle/Resources/views/layout.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/layout.html.twig @@ -4,30 +4,30 @@ - - - - - {% block title %}{% endblock %} - wallabag - {% include "WallabagCoreBundle::_head.html.twig" %} - {% include "WallabagCoreBundle::_bookmarklet.html.twig" %} - - -{% include "WallabagCoreBundle::_top.html.twig" %} -
    - {% block menu %}{% endblock %} - {% block precontent %}{% endblock %} - {% for flashMessage in app.session.flashbag.get('notice') %} -
    - {{ flashMessage }} + + + + + {% block title %}{% endblock %} - wallabag + {% include "WallabagCoreBundle::_head.html.twig" %} + {% include "WallabagCoreBundle::_bookmarklet.html.twig" %} + + + {% include "WallabagCoreBundle::_top.html.twig" %} +
    + {% block menu %}{% endblock %} + {% block precontent %}{% endblock %} + {% for flashMessage in app.session.flashbag.get('notice') %} +
    + {{ flashMessage }} +
    + {% endfor %} +
    + {% block content %}{% endblock %}
    - {% endfor %} -
    - {% block content %}{% endblock %}
    -
    -{% include "WallabagCoreBundle::_footer.html.twig" %} - + {% include "WallabagCoreBundle::_footer.html.twig" %} + \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/Security/Authentication/Provider/WsseProvider.php b/src/Wallabag/CoreBundle/Security/Authentication/Provider/WsseProvider.php new file mode 100644 index 000000000..ac57e27df --- /dev/null +++ b/src/Wallabag/CoreBundle/Security/Authentication/Provider/WsseProvider.php @@ -0,0 +1,59 @@ +userProvider = $userProvider; + $this->cacheDir = $cacheDir; + } + + public function authenticate(TokenInterface $token) + { + $user = $this->userProvider->loadUserByUsername($token->getUsername()); + + if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) { + $authenticatedToken = new WsseUserToken($user->getRoles()); + $authenticatedToken->setUser($user); + + return $authenticatedToken; + } + + throw new AuthenticationException('The WSSE authentication failed.'); + } + + protected function validateDigest($digest, $nonce, $created, $secret) + { + // Expire le timestamp après 5 minutes + if (time() - strtotime($created) > 300) { + return false; + } + + // Valide que le nonce est unique dans les 5 minutes + if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) { + throw new NonceExpiredException('Previously used nonce detected'); + } + file_put_contents($this->cacheDir.'/'.$nonce, time()); + + // Valide le Secret + $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true)); + + return $digest === $expected; + } + + public function supports(TokenInterface $token) + { + return $token instanceof WsseUserToken; + } +} \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/Security/Authentication/Token/WsseUserToken.php b/src/Wallabag/CoreBundle/Security/Authentication/Token/WsseUserToken.php new file mode 100644 index 000000000..b189e22a5 --- /dev/null +++ b/src/Wallabag/CoreBundle/Security/Authentication/Token/WsseUserToken.php @@ -0,0 +1,23 @@ +setAuthenticated(count($roles) > 0); + } + + public function getCredentials() + { + return ''; + } +} \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/Security/Firewall/WsseListener.php b/src/Wallabag/CoreBundle/Security/Firewall/WsseListener.php new file mode 100644 index 000000000..b24747858 --- /dev/null +++ b/src/Wallabag/CoreBundle/Security/Firewall/WsseListener.php @@ -0,0 +1,58 @@ +securityContext = $securityContext; + $this->authenticationManager = $authenticationManager; + } + + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + + $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/'; + if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) { + return; + } + + $token = new WsseUserToken(); + $token->setUser($matches[1]); + + $token->digest = $matches[2]; + $token->nonce = $matches[3]; + $token->created = $matches[4]; + + try { + $authToken = $this->authenticationManager->authenticate($token); + + $this->securityContext->setToken($authToken); + } catch (AuthenticationException $failed) { + // ... you might log something here + + // To deny the authentication clear the token. This will redirect to the login page. + // $this->securityContext->setToken(null); + // return; + + // Deny authentication with a '403 Forbidden' HTTP response + $response = new Response(); + $response->setStatusCode(403); + $event->setResponse($response); + + } + } +} \ No newline at end of file diff --git a/src/Wallabag/CoreBundle/WallabagCoreBundle.php b/src/Wallabag/CoreBundle/WallabagCoreBundle.php index f5899e39c..1deab03a9 100644 --- a/src/Wallabag/CoreBundle/WallabagCoreBundle.php +++ b/src/Wallabag/CoreBundle/WallabagCoreBundle.php @@ -3,7 +3,16 @@ namespace Wallabag\CoreBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Wallabag\CoreBundle\DependencyInjection\Security\Factory\WsseFactory; +use Symfony\Component\DependencyInjection\ContainerBuilder; class WallabagCoreBundle extends Bundle { + public function build(ContainerBuilder $container) + { + parent::build($container); + + $extension = $container->getExtension('security'); + $extension->addSecurityListenerFactory(new WsseFactory()); + } }