I must implement and integrate a SAML2 Identity Provider (IdP) with an existing Symfony 2 application.
I found some bundle that implement Service Provider (SP) but not Identity Provider so I think that I may use SimpleSAMLphp library. Are there other solutions?
How can I integrate my user provider logic with SimpleSAMLphp?
UPDATE
As Milos Tomic mentioned in his comment, aerialship/lightsaml is replaced by lightsaml/sp-bundle. You can find an introduction here.
+++++++++++++++++++++++++++
I have recently set up a SAML Solution using Simplesamlphp as IDP and SamlSPBundle as SP and everything is working well.
I recommend installing Simplesamlphp first, following this good Documentation here.
Once you have the IDP up and running, you should see a Welcome page and a Tab called Federation (or something like that, my Installation is in german). There you should see one option "SAML 2.0 IdP Metadata". Follow that link and copy the XML shown to a seperate file and save that file.
On the symfony side, I created a new Bundle and called that "SamlBundle". Download and install SamlSPBundle as described in their Documentation (Step 1 and Step 2).
Create your SSO State/User class (Step 3). Here is an example how I did it:
namespace SamlBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity
* #ORM\Table(name="samlUser")
*/
class SamlUser extends \AerialShip\SamlSPBundle\Entity\SSOStateEntity implements UserInterface
{
/**
* initialize User object and generates salt for password
*/
public function __construct()
{
if (!$this->userData instanceof UserData) {
$this->userData = new UserData();
}
$this->setRoles('ROLE_USER');
}
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string username
*
* #ORM\Column(type="string", length=64, nullable=true)
*/
protected $username;
/**
* #var string targetedId
*
* #ORM\Column(type="string", length=64, nullable=true, name="targeted_id")
*/
protected $targetedID;
/**
* #var string
* #ORM\Column(type="string", length=32, name="provider_id", nullable=true)
*/
protected $providerID;
/**
* #var string
* #ORM\Column(type="string", length=32, name="auth_svc_name")
*/
protected $authenticationServiceName;
/**
* #var string
* #ORM\Column(type="string", length=64, name="session_index", nullable=true)
*/
protected $sessionIndex;
/**
* #var string
* #ORM\Column(type="string", length=64, name="name_id")
*/
protected $nameID;
/**
* #var string
* #ORM\Column(type="string", length=64, name="name_id_format")
*/
protected $nameIDFormat;
/**
* #var \DateTime
* #ORM\Column(type="datetime", name="created_on")
*/
protected $createdOn;
/**
* #var UserData
* #ORM\OneToOne(targetEntity="UserData", cascade={"all"}, fetch="EAGER")
* #ORM\JoinColumn(name="user_data", referencedColumnName="id")
*/
protected $userData;
Add your class to config.yml (Step 4):
# app/config/config.yml
aerial_ship_saml_sp:
driver: orm
sso_state_entity_class: SamlBundle\Entity\SamlUser
Update your security.yml (Step 5). Example;
providers:
saml_user_provider:
id: SamlToState
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
saml:
pattern: ^/(?!login_check)
anonymous: true
aerial_ship_saml_sp:
login_path: /saml/sp/login
check_path: /saml/sp/acs
logout_path: /saml/sp/logout
failure_path: /saml/sp/failure
metadata_path: /saml/sp/FederationMetadata.xml
discovery_path: /saml/sp/discovery
local_logout_path: /logout
provider: saml_user_provider
create_user_if_not_exists: true
services:
openidp:
idp:
#the XML-File you saved from the IDP earlier
file: "#SamlBundle/Resources/idp-FederationMetadata.xml"
sp:
config:
# required, has to match entity id from IDP XML
entity_id: http://your-idp-domain.com
# if different then url being used in request
# used for construction of assertion consumer and logout urls in SP entity descriptor
base_url: http://your-sp-domain.com
signing:
#self signed certificate, see [SamlSPBundle docs][4]
cert_file: "#SamlBundle/Resources/saml.crt"
key_file: "#SamlBundle/Resources/saml.pem"
key_pass: ""
meta:
# must implement SpMetaProviderInterface
# id: my.sp.provider.service.id
# or use builtin SpMetaConfigProvider
# any valid saml name id format or shortcuts: persistent or transient
name_id_format: transient
binding:
# any saml binding or shortcuts: post or redirect
authn_request: redirect
logout_request: redirect
logout:
path: /logout
target: /
invalidate_session: true
Next import the routes as described in Step 6. Before you go on to Step 7, I recommend to create your User provider class first. Here is an example:
namespace SamlBundle\Models;
use SamlBundle\Entity\SamlUser;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use AerialShip\SamlSPBundle\Bridge\SamlSpInfo;
use AerialShip\SamlSPBundle\Security\Core\User\UserManagerInterface as UserManagerInterface;
class SamlToState implements UserManagerInterface
{
/**
* #var ContainerInterface base bundle container
*/
public $container;
/**
* Constructor with DependencyInjection params.
*
* #param \Symfony\Component\DependencyInjection\ContainerInterface $container
*/
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
/**
* {#inheritdoc}
*/
public function loadUserBySamlInfo(SamlSpInfo $samlInfo)
{
$user = $this->loadUserByTargetedID($samlInfo->getAttributes()['eduPersonTargetedID']->getFirstValue());
return $user;
}
private function loadUserByTargetedID($targetedID) {
$repository = $this->container->get('doctrine')->getManager()->getRepository('MrmPosSamlBundle:SamlUser');
$user = $repository->findOneBy(
array('targetedID' => $targetedID)
);
if ($user) {
return $user;
}
throw new \Symfony\Component\Security\Core\Exception\UsernameNotFoundException();
}
/**
* {#inheritdoc}
*/
public function createUserFromSamlInfo(SamlSpInfo $samlInfo)
{
$repository = $this->container->get('doctrine')->getManager()->getRepository('MrmPosSamlBundle:SamlUser');
$user = $repository->findOneBy(
array('nameID' => $samlInfo->getNameID()->getValue())
);
if ($user) {
$user->setUsername($samlInfo->getAttributes()['eduPersonPrincipalName']->getFirstValue());
$user->setTargetedID($samlInfo->getAttributes()['eduPersonTargetedID']->getFirstValue());
$user->setRoles($samlInfo->getAttributes()['role']->getFirstValue());
} else {
$user = new SamlUser();
$user->setUsername($samlInfo->getAttributes()['eduPersonPrincipalName']->getFirstValue());
$user->setTargetedID($samlInfo->getAttributes()['eduPersonTargetedID']->getFirstValue());
$user->setRoles($samlInfo->getAttributes()['role']->getFirstValue());
$user->setSessionIndex($samlInfo->getAuthnStatement()->getSessionIndex());
$user->setProviderID($samlInfo->getNameID()->getSPProvidedID());
$user->setAuthenticationServiceName($samlInfo->getAuthenticationServiceID());
$user->setNameID($samlInfo->getNameID()->getValue());
$user->setNameIDFormat($samlInfo->getNameID()->getFormat());
}
$em = $this->container->get('doctrine')->getManager();
$em->persist($user);
$em->flush();
return $user;
}
public function loadUserByUsername($username)
{
$repository = $this->container->get('doctrine')->getManager()->getRepository('MrmPosSamlBundle:SamlUser');
$user = $repository->findOneBy(
array('username' => $username)
);
if ($user) {
return $user;
}
throw new \Symfony\Component\Security\Core\Exception\UsernameNotFoundException();
return false;
}
/**
* {#inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
$repository = $this->container->get('doctrine')->getManager()->getRepository('MrmPosSamlBundle:SamlUser');
$newUser = $repository->findOneBy(
array('nameID' => $user->getNameID())
);
if (!$newUser) {
throw new \Symfony\Component\Security\Core\Exception\UsernameNotFoundException();
}
return $newUser;
}
/**
* {#inheritdoc}
*/
public function supportsClass($class)
{
return true;
}
}
Create your service in SamlBundle/Resources/config/services.yml:
services:
SamlToState:
class: SamlBundle\Models\SamlToState
arguments: [#service_container]
Now its time for Step 7, exchanging Metadata. get the SP XML as described and go back to your IDP. You find a "XML to simpleSAMLphp Metadata converter" Link on the Federation Tab. Follow that link and convert your SP XML Data to the simpleSAMLphp format. Add that data to the saml20-sp-remote.php file in your IDPs metadata folder.
Ok, I'm pretty sure that I forgot something, but hopefully this information helps. If you get stuck, your welcome to get back to me.
I found:
SURFnet SamlBundle that is a simple symfony2 wrapper of the library you found.
SamlSpBundle more used and well documented.
Take a look at this two. The first is very simple and i don't know if is sufficient documented, of course is an active project. The second seem more powerful and documented but can be more difficult to configure.
Hope this help
Related
I have to create a simple user administration for a symfony 3 project.
One part of it is to start the password reset process for users.
(Yes, I know every user can trigger it himself but this is a request from our customer.)
Now I don't know how to start the process with a simple click in the admin interface for every user. Is there a method or a service in the UserBundle I can use?
There is no all in one method but this can be achieved by:
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
// send email you requested
$this->mailer->sendResettingEmailMessage($user);
// this depends on requirements
$user->setPasswordRequestedAt(new \DateTime());
$this->userManager->updateUser($user);
with proper dependencies set.
Here's a service based solution written with Symfony 4.1 you can use without having to call in services form the container via get()
First you have to add an alias to services.yaml because the FOS mailer can't auto-wire:
FOS\UserBundle\Mailer\Mailer:
alias: fos_user.mailer.default
public: true
With that in place you can create the below class as service:
namespace App\Service; # change to your namespace
use FOS\UserBundle\Mailer\Mailer;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Util\TokenGeneratorInterface;
/**
* Class UserPasswordResetService
*/
class UserPasswordResetService
{
/**
* #var Mailer
*/
private $mailer;
/**
* #var UserManagerInterface
*/
private $userManager;
/**
* #var TokenGeneratorInterface
*/
private $tokenGenerator;
/**
* UserPasswordResetService constructor.
*
* #param Mailer $mailer
* #param UserManagerInterface $userManager
*/
public function __construct(
Mailer $mailer,
UserManagerInterface $userManager,
TokenGeneratorInterface $tokenGenerator
)
{
$this->mailer = $mailer;
$this->userManager = $userManager;
$this->tokenGenerator = $tokenGenerator;
}
/**
* #param UserInterface $user
*/
public function resetPassword(UserInterface $user)
{
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
// send email you requested
$this->mailer->sendResettingEmailMessage($user);
// this depends on requirements
$user->setPasswordRequestedAt(new \DateTime());
$this->userManager->updateUser($user);
}
}
Assuming you then add that service to a class via DI you can use it like this within a given method:
$this->passwordResetService->resetPassword($user);
From the information Kamil provided, this would be a full working function
/**
* Sends the user a new password
*
* #Route("reset_password/{id}", name="user_reset_password")
* #Security("has_role('ROLE_ADMIN')")
*
* #param User $user
* #return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function resetPasswordAction(User $user)
{
if (null === $user->getConfirmationToken()) {
/** #var $tokenGenerator TokenGeneratorInterface */
$tokenGenerator = $this->get('fos_user.util.token_generator');
$user->setConfirmationToken($tokenGenerator->generateToken());
}
$this->get('fos_user.mailer')->sendResettingEmailMessage($user);
$user->setPasswordRequestedAt(new \DateTime());
$this->get('fos_user.user_manager')->updateUser($user);
$this->addFlash('notice', "User {$user->getFullName()} got an email for resetting his password!");
return $this->redirectToRoute('user_index');
}
I'm writing a functional test for an Action entity having a relationship with the User entity:
<?php
namespace Acme\AppBundle\Entity;
/**
* Class Action
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Acme\AppBundle\Repository\ActionRepository")
*/
class Action
{
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Acme\AppBundle\Entity\User
*
* #ORM\ManyToOne(targetEntity="\Acme\AppBundle\Entity\User", inversedBy="actions")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $createdBy;
}
User:
namespace Acme\AppBundle\Entity;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Action", mappedBy="createdBy")
*/
private $actions;
}
And the user is setted in the controller with the following snippet:
<?php
namespace Acme\ApiBundle\Controller;
/**
*
* #Route("/actions")
*/
class ActionController extends FOSRestController
{
public function postAction(Request $request)
{
$action = new Action();
$action->setCreatedBy($this->getUser());
return $this->processForm($action, $request->request->all(), Request::METHOD_POST);
}
}
When calling the action with a REST client for example, everything works fine, the relationship between Action and User is persisted correctly.
Now, when testing the action with a functional test, the relationship is not working because of the following error:
A new entity was found through the relationship 'Acme\AppBundle\Entity\Action#createdBy' that was not configured to cascade persist operations for entity: test. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
For my functional test I need to inject a JWT and a session token because my routes are secured by a JWT and I need to have a user in session.
Here is how I inject that:
<?php
namespace Acme\ApiBundle\Tests;
class ApiWebTestCase extends WebTestCase
{
/**
* #var ReferenceRepository
*/
protected $fixturesRepo;
/**
* #var Client
*/
protected $authClient;
/**
* #var array
*/
private $fixtures = [];
protected function setUp()
{
$fixtures = array_merge([
'Acme\AppBundle\DataFixtures\ORM\LoadUserData'
], $this->fixtures);
$this->fixturesRepo = $this->loadFixtures($fixtures)->getReferenceRepository();
$this->authClient = $this->createAuthenticatedClient();
}
/**
* Create a client with a default Authorization header.
*
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->fixturesRepo->getReference('user-1');
$jwtManager = $this->getContainer()->get('lexik_jwt_authentication.jwt_manager');
$token = $jwtManager->create($user);
$this->loginAs($user, 'api');
$client = static::makeClient([], [
'AUTHENTICATION' => 'Bearer ' . $token,
'CONTENT_TYPE' => 'application/json'
]);
$client->disableReboot();
return $client;
}
}
Now, the issue is that the injected UsernamePasswordToken contains a User instance which is detached from the current EntityManager, thus resulting in the Doctrine error above.
I could merge the $user object in the postAction method into the EntityManager but I don't want to do that because it means I modify my working code to make a test passes.
I've also tried directling merging the $user object in my test into the EntityManager like this:
$em = $client->getContainer()->get('doctrine')->getManager();
$em->merge($user);
But it's not working either.
So now, I'm stuck, I really don't know what to do except that I need to attach the user in session back to the current EntityManager.
The error message you are getting indicates that the EntityManager contained in the test client's container doesn't know about your User entity. This leads me to believe that the way you are retrieving the User in your createAuthenticatedClient method is using a different EntityManager.
I suggest you try to use the test kernel's EntityManager to retrieve the User entity instead. You can get it from the test client's container, for example.
Thanks to your tweet, I come to complete the given answer and (try to) propose a solution,
The problem is that your user is not managed by the EntityManager, and more simply, because it's not a real existing user that is registered in database.
To get around this problem, you need to have a real (managed) user that doctrine could use for the association that your action is trying to create.
So, you can either create this user at each execution of your functional test case (and delete it when finished), or create it only once when execute the test case for the first time on a new environment.
Something like this should do the trick:
/** #var EntityManager */
private $em;
/**
*/
public function setUp()
{
$client = static::createClient();
$this->em = $client->getKernel()
->getContainer()
->get('doctrine');
$this->authClient = $this->createAuthenticatedClient();
}
/**
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->em
->getRepository('Acme\AppBundle\Entity\User')
->findOneBy([], ['id' => DESC]; // Fetch the last created
// ...
return $client;
}
That's a pity for your fixtures (that are so much sexier), but I don't see any way to attach your fixture as a real entry, as you can't interact more with the tested controller.
Another way would be to create a request to your login endpoint, but it would be even more ugly.
I'm using FOSUserBundle and I want when each user registers to be disabled by default. The administrator will contact every user by phone and he will make user active if it's appropriate. I have read about Overriding Default FOSUserBundle Controllers but I can't figure out how to make it working. I have created RegistrationController.php in src/AppBundle/Controller/RegistrationController.php with this method inside:
<?php
/*
* This file is part of the FOSUserBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\UserBundle\Controller;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use FOS\UserBundle\Model\UserInterface;
/**
* Controller managing the registration
*
* #author Thibault Duplessis <thibault.duplessis#gmail.com>
* #author Christophe Coevoet <stof#notk.org>
*/
class RegistrationController extends ContainerAware
{
/**
* Receive the confirmation token from user email provider, login the user
*/
public function confirmAction($token)
{
$user = $this->container->get('fos_user.user_manager')->findUserByConfirmationToken($token);
if (null === $user) {
throw new NotFoundHttpException(sprintf('The user with confirmation token "%s" does not exist', $token));
}
$user->setConfirmationToken(null);
$user->setEnabled(false);
$user->setLastLogin(new \DateTime());
$this->container->get('fos_user.user_manager')->updateUser($user);
$response = new RedirectResponse($this->container->get('router')->generate('fos_user_registration_confirmed'));
$this->authenticateUser($user, $response);
return $response;
}
}
, but nothing works, maybe I need someone to show me the way to do it, nothing more.
For those still struggling with this question, set an observer listening the event: fos_user.registration.initialize like this (adapt you code path) :
app.listener.disable_registered_user:
class: AppBundle\Observer\DisableRegisteredUserListener
arguments:
- "#templating"
tags:
# split to multiple line for readability
# can be made into a single line like - { name: ..., event: ... , method: ... }
-
name: "kernel.event_listener"
event: "fos_user.registration.initialize"
method: "disableUser"
Then this is the content of your event listener class :
namespace AppBundle\Observer;
use FOS\UserBundle\Event\GetResponseUserEvent;
/**
* Class DisableRegisteredUserListener
* #package AppBundle\Observer
*/
class DisableRegisteredUserListener
{
/**
* #param \FOS\UserBundle\Event\GetResponseUserEvent $event
*/
public function disableUser(GetResponseUserEvent $event)
{
$user = $event->getUser();
/** #var \AppBundle\Entity\User $user */
$user->setEnabled(false);
}
}
You could just listen to the FOSUserEvents::REGISTRATION_CONFIRM and disable the registered user again before it gets persisted to the database.
As the FOSUserBundle automatically forwards the new user to the confirmedAction that requires a user to be logged in, you would need to provide your own response to override this.
Your listener...
class DisableRegisteredUserListener
{
/**
* #var EngineInterface
*/
private $templating;
/**
* #var EngineInterface $templating
*/
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
/**
* #var GetResponseUserEvent $event
* #return null
*/
public function disableUser(GetResponseUserEvent $event)
{
$user = $event->getUser();
$user->setEnabled(false);
$response = $this->templating->renderResponse(
'AppBundle:Registration:registration_complete.html.twig',
array(
'user' => $user,
)
);
}
}
Your services file (YAML)...
services:
app.listener.disable_registered_user:
class: AppBundle\EventListener\DisableRegisteredUserListener
arguments:
- "#templating"
tags:
# split to multiple line for readability
# can be made into a single line like - { name: ..., event: ... , method: ... }
-
name: "kernel.event_listener"
event: "fos_user.registration.confirm"
method: "disableUser"
Your AppBundle:Registration:registration_complete.html.twig could then be used to tell the new users that their account had been created but disabled and they would then be contacted by a member of your team to complete the process.
I have a doctrine entity User and a document Address (stored in mongoDB). I want to set an one to many relation between them by userId property. (the user has many addresses)
My User Entity:
namespace BlaBla\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #var integer
*/
private $id;
/**
* #var string
*/
private $firstName;
... and so on
My Address document:
namespace BlaBla\UserBundle\Document;
/**
* BlaBla\UserBundle\Document\Address
*/
class Address
{
/**
* #var MongoId $id
*/
protected $id;
/**
* #var string $firstName
*/
protected $firstName;
/**
* #var string $lastName
*/
protected $lastName;
/**
* #var int $userId
*/
protected $userId;
... and so on
My goal is to create the getUser() method for the Address object and the getAddresses() method for the User object.
I've decided to place the method getAddresses() to the doctrine UserRepository class and to inject there the necessary document manager to be able to access to the Address Document. I've overriden the constructor of the userRepository and passed to it the necessary document manager object.
Please, look to the UserRepository class:
<?php
namespace BlaBla\UserBundle\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
/**
* #var \Doctrine\ODM\MongoDB\DocumentManager
*/
private $_dm;
/**
* #param \Doctrine\ORM\EntityManager $dm
*/
public function __construct(\Doctrine\ORM\EntityManager $em, $dm) {
$metaData = new \Doctrine\ORM\Mapping\ClassMetadata('BlaBla\UserBundle\Entity\User');
parent::__construct($em, $metaData);
$this->_dm = $dm;
}
/**
* #param $user_id integer
* #return \BlaBla\UserBundle\Document\Address
*/
public function getAddress($user_id) {
$address = $this->_dm->getRepository('BlaBlaUserBundle:Address');
$rt = $address->findByUserId($user_id);
return $rt;
}
public function getAllUsers()
{
return $this->findAll();
}
}
After this I can access to the repository from my controller via:
$em = $this->getDoctrine()->getManager();
$dm = $this->get('doctrine_mongodb')->getManager();
$t = new \BlaBla\UserBundle\Repository\UserRepository($em, $dm);
var_dump($t->getAddress($id));
var_dump($t->getAllUsers());
Both methods work just fine, but now I can't access to the repository using shortcuts like:
$user = $this->getDoctrine()->getRepository('BlaBlaUserBundle:User');
I thought about making the Repository as service with something like this:
user.repository:
class: BlaBla\UserBundle\Repository\UserRepository
arguments: [#doctrine.orm.entity_manager, #doctrine.odm.mongo_db.document_manager]
in my services.yml file, but this only lets me to access the repository with:
$this->get('user.repository');
the default shortcuts doesn't work still.
Please help to find a correct solution for this problem.
Thanks.
Where did you specified the UserRepository? In your User.php with annotation ? Maybe that is the only thing what is missing.
But if you want to use entity and document repository, I advise you to use Doctrine extensions, specifically Reference.
I have a symfony app which serve a RESTful API(for mobile app) and have backend administration.
I can succesfuly login to the backend via facebook, but how should I allow loggin via the RESTful API?
Wooh.. after almost 12 hours(!) here is the solution for anyone who looking for too:
We need to create new custom firewall
This factory should connect to the FOSFacebook and validate the token
If it using our new firewall it should manually disable any session or cookie.
To use the firewall we need to send our token in every request
The code
First define our firewall listener
GoDisco/UserBundle/Security/Firewall/ApiFacebookListener.php
<?php
/**
* Authored by AlmogBaku
* almog.baku#gmail.com
* http://www.almogbaku.com/
*
* 9/6/13 2:17 PM
*/
namespace Godisco\UserBundle\Security\Firewall;
use FOS\FacebookBundle\Security\Authentication\Token\FacebookUserToken;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
/**
* API gateway through Facebook oAuth token: Firewall
*
* Class ApiFacebookListener
* #package Godisco\UserBundle\Security\Firewall
*/
class ApiFacebookListener implements ListenerInterface
{
/**
* #var \Symfony\Component\Security\Core\SecurityContextInterface
*/
protected $securityContext;
/**
* #var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
*/
protected $authenticationManager;
/**
* #var Session
*/
protected $session;
/**
* #var string
*/
protected $providerKey;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, Session $session, $providerKey)
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->session = $session;
$this->providerKey=$providerKey;
}
/**
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event The event.
*/
public function handle(GetResponseEvent $event)
{
$accessToken = $event->getRequest()->get('access_token');
$token = new FacebookUserToken($this->providerKey, '', array(), $accessToken);
/**
* force always sending token
*/
$_COOKIE=array();
$this->session->clear();
try {
if($accessToken)
$returnValue = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($returnValue);
}
} catch(AuthenticationException $exception) {
if(!empty($accessToken))
$event->setResponse(new Response(array("error"=>$exception->getMessage()),401));
}
}
}
Than create a new security factory which calling our listener, and will connect the authentication to the FOSFacebookBundle.
GoDisco/UserBundle/DependencyInjection/Security/Factory/ApiFacebookFactory.php
<?php
/**
* Authored by AlmogBaku
* almog.baku#gmail.com
* http://www.almogbaku.com/
*
* 9/6/13 2:31 PM
*/
namespace GoDisco\UserBundle\DependencyInjection\Security\Factory;
use FOS\FacebookBundle\DependencyInjection\Security\Factory\FacebookFactory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
/**
* API gateway through Facebook oAuth token: Factory
*
* Class ApiFacebookFactory
* #package GoDisco\UserBundle\DependencyInjection\Security\Factory
*/
class ApiFacebookFactory extends FacebookFactory
{
/**
* {#inheritdoc}
*/
public function getKey()
{
return 'api_facebook';
}
/**
* {#inheritdoc}
*/
public function addConfiguration(NodeDefinition $node)
{
$builder = $node->children();
$builder
->scalarNode('provider')->end()
->booleanNode('remember_me')->defaultFalse()->end()
;
foreach ($this->options as $name => $default) {
if (is_bool($default)) {
$builder->booleanNode($name)->defaultValue($default);
} else {
$builder->scalarNode($name)->defaultValue($default);
}
}
}
/**
* {#inheritdoc}
*/
protected function createEntryPoint($container, $id, $config, $defaultEntryPointId)
{
return null;
}
/**
* {#inheritdoc}
*/
protected function createListener($container, $id, $config, $userProvider)
{
$listenerId = "api_facebook.security.authentication.listener";
$listener = new DefinitionDecorator($listenerId);
$listener->replaceArgument(3, $id);
$listenerId .= '.'.$id;
$container->setDefinition($listenerId, $listener);
return $listenerId;
}
}
Defining the listener service, so we can inject the arguments
GoDisco/UserBundle/Resources/config/services.yml
services:
api_facebook.security.authentication.listener:
class: GoDisco\UserBundle\Security\Firewall\ApiFacebookListener
arguments: ['#security.context', '#security.authentication.manager', '#session', '']
Defining our new firewall!
app/config/security.yml
security:
api:
pattern: ^/api
api_facebook:
provider: godisco_facebook_provider
stateless: true
anonymous: true
main:
...
You need to implement oAuth authentication from your client app.
This was answered before:
How to restfully login, Symfony2 Security, FOSUserBundle, FOSRestBundle?