I'm implementing a custom authentication provider for using an external api following roughly the cookbook on the symfony website.
It works almost everything correctly, the listener listens the login form properly, then it calls the authenticate function which returns the authenticated token, the problem is that even if i set a authenticated token to the securityContextInterface, the system returns to the login page with wrong credentials.
Under the code i've used
What could it be?
security.yml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/app/login$
security: false
api_secured:
provider: in_memory
pattern: ^/app
form_login:
login_path: /app/login
check_path: /app/login_check
logout:
path: /app/logout
target: /
api: true
services.yml
api.security.authentication.provider:
class: Manuel\Myapp\MyAppBundle\Security\Authentication\Provider\ApiProvider
arguments: ['', %kernel.cache_dir%/security/nonces]
api.security.authentication.listener:
class: Manuel\Myapp\MyAppBundle\Security\Firewall\ApiListener
arguments: [#security.context, #security.authentication.manager, %api.url%]
ApiFactory.php
namespace Manuel\Myapp\MyAppBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
class ApiFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.api.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('api.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.api.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('api.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'api';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
ApiListener.php
namespace Manuel\Myapp\MyAppBundle\Security\Firewall;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Manuel\Myapp\MyAppBundle\Security\Authentication\Token\ApiUserToken;
use Httpful\Request;
class ApiListener implements ListenerInterface {
protected $securityContext;
protected $authenticationManager;
protected $container;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, $api)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
//Prendo l'url delle api
//Viene passato da services.yml alla classe
$this->api = $api;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$data = $request->request->all();
//Esiste username e password ?
if(!array_key_exists('_username', $data) || !array_key_exists('_password', $data)) {
//Ritorna alla pagina di login con bad credentials
$this->securityContext->setToken(null);
return;
}
//Autentico in remoto
$username = $data['_username'];
$password = $data['_password'];
$response = Request::post($this->api."/token/new.json")
->body(array(
'username'=> $username,
'password'=> $password))
->expectsJson()
->sendsForm()
->send();
$decode = json_decode($response);
//Se esiste allora vado avanti se no muoio
if(!$decode->success) {
$this->securityContext->setToken(null);
return;
}
$token = new ApiUserToken();
$token->setUser(''.$decode->user);
$token->token = $decode->token;
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
} catch (AuthenticationException $failed) {
// ... si potrebbe loggare qualcosa in questo punto
// Per negare l'autenticazione, pulire il token. L'utente sarĂ rinviato alla pagina di login.
$this->securityContext->setToken(null);
return;
// Negare l'autenticazione con una risposta HTTP '403 Forbidden'
//$response = new Response();
//$response->setStatusCode(403);
//$event->setResponse($response);
}
}
}
If i write:
$authToken = $this->authenticationManager->authenticate($token);
var_dump($authToken); die();
$this->securityContext->setToken($authToken);
The results is:
object(Manuel\Myapp\MyAppBundle\Security\Authentication\Token\ApiUserToken)#4780 (5) {["user":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> object(Symfony\Component\Security\Core\User\User)#4782 (7) { ["username":"Symfony\Component\Security\Core\User\User":private]=> string(4) "user" ["password":"Symfony\Component\Security\Core\User\User":private]=> string(15) "10dmao!?postino" ["enabled":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["accountNonExpired":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["credentialsNonExpired":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["accountNonLocked":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["roles":"Symfony\Component\Security\Core\User\User":private]=> array(1) { [0]=> string(9) "ROLE_USER" } } ["roles":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> array(1) { [0]=> object(Symfony\Component\Security\Core\Role\Role)#4779 (1) { ["role":"Symfony\Component\Security\Core\Role\Role":private]=> string(9) "ROLE_USER" } } ["authenticated":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> bool(true) ["attributes":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> array(0) { } }
So it is correct.
ApiUserToken.php
namespace Manuel\Myapp\MyAppBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class ApiUserToken extends AbstractToken
{
public $token;
public function __construct(array $roles = array())
{
parent::__construct($roles);
// If the user has roles, consider it authenticated
$this->setAuthenticated(true);
}
public function getCredentials()
{
return '';
}
}
ApiProvider.php
namespace Manuel\Myapp\MyAppBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Manuel\Myapp\MyAppBundle\Security\Authentication\Token\ApiUserToken;
class ApiProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
}
public function authenticate(TokenInterface $token)
{
//Devo aggiungere utente
$user = $this->userProvider->loadUserByUsername("user");
if ($user) {
$authenticatedToken = new ApiUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The API authentication failed.');
}
public function supports(TokenInterface $token) {
return $token instanceof ApiUserToken;
}
}
I've resolved modifying security.yml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/app/login$
security: false
secured_area:
pattern: ^/app
api: true
logout:
path: /app/logout
target: /
and ApiListener.php
public function handle(GetResponseEvent $event) {
if( $this->securityContext->getToken() ){
return;
}
Because on every url under firewall (app/*) symfony calls the handle method of my listener, if the user is already logged the security token is already setted and I return
and check_login function
public function securityCheckAction() {
// The security layer will NOT intercept this request
return $this->redirect($this->generateUrl('manuel_myapp_index_after_login'));
check_login is the action of the login form, the check_login action is under firewall, so, the handle method of my listerner will be called for the first time, if credentials are correct (using my external api) I forced symfony to use the in_memory user for login and than the check_login action will be executed.
Then, when the user visit another page under firewall, the handle method will be recalled but the authentication token is already setted, so the handle method will return and all works
External api login now works !
Related
I have to use LoginForm and RegistrationForm in the same page
I m using classic Guard Authentication provided by make:auth
Based on Symfony 5 - Multiples forms on same page, I have created LoginFormType and copy what I have in RegistrationController.
Both Login and Registration fails.
security.yaml :
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
lazy: true
provider: app_user_provider
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
target: app_login
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
Security/LoginFormAuthenticator.php
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
/**
* #var $entityManager EntityManagerInterface
*/
private $entityManager;
/**
* #var UrlGeneratorInterface
*/
private $urlGenerator;
/**
* #var $csrfTokenManager CsrfTokenManagerInterface
*/
private $csrfTokenManager;
/**
* #var $passwordEncoder UserPasswordEncoderInterface
*/
private $passwordEncoder;
public function __construct(
EntityManagerInterface $entityManager,
UrlGeneratorInterface $urlGenerator,
CsrfTokenManagerInterface $csrfTokenManager,
UserPasswordEncoderInterface $passwordEncoder
) {
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request)
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
}
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
dd('hello');
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('dashboard'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
SecurityController login method
public function login(
Request $request,
AuthenticationUtils $authenticationUtils,
GuardAuthenticatorHandler $guardAuthenticatorHandler,
LoginFormAuthenticator $loginFormAuthenticator
): Response {
if ($this->getUser()) {
return $this->redirectToRoute('dashboard');
}
// LOGIN
$userToLogIn = new User();
$director = new Director();
$loginForm = $this->createForm(LoginFormType::class, $userToLogIn);
$registrationForm = $this->createForm(RegistrationFormType::class, $director);
if ($request->isMethod(Request::METHOD_POST)) {
$loginForm->handleRequest($request);
$registrationForm->handleRequest($request);
dump($request->get('signIn'));
dd($request->get('signUp'));
}
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
'loginForm' => $loginForm->createView(),
'registrationForm' => $registrationForm->createView()
]);
}
I m using:
Symfony 5.1.3
PHP 7.3.20 ( I m not upgraded to 7.4 yet for non compatibility of some vendors)
I m using Symfony local server, no nginx or apache
I don't have any .htaccess file
When I m googling that, I found that could be relative to session, that's why I added
framework.yaml :
framework:
//...
session:
//...
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
Login and registration worked before separately
But now when I just visit any page in my site
I have this
and This when I try to login
Any Help please ???
I solved the problem by rendering the registration controller in the login template
login.html.twig
// login form
....
{{ render(controller('App\\Controller\\RegistrationController::register')) }}
I want to achieve the following:
In my installation there are two bundles, ApiBundle and BackendBundle. Users are defined in BackendBundle, though I could put them in a UserBundle later.
ApiBundle basically provides a controller with api methods like for example getSomething().
BackendBundle has the user entities, services and some views like a login form and a backend view. From the backend controller I would want to access certain api methods.
Other api methods will be requested from outside. Api methods will be requested through curl.
I would want to have different users for both purposes. The User class implements UserInterface and has properties like $username, $password and $apiKey.
Now basically I want to provide an authentication method through login form with username and password, and another authentication method for api calls through curl from outside, that only will require the apiKey.
In both cases, the authenticated user then should have access to different ressources.
My security.yml so far looks like this:
providers:
chain_provider:
chain:
providers: [db_username, db_apikey]
db_username:
entity:
class: BackendBundle:User
property: username
db_apikey:
entity:
class: BackendBundle:User
property: apiKey
encoders:
BackendBundle\Entity\User:
algorithm: bcrypt
cost: 12
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
form_login:
login_path: login
check_path: login
default_target_path: backend
csrf_token_generator: security.csrf.token_manager
logout:
path: /logout
target: /login
provider: chain_provider
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: ROLE_API }
- { path: ^/backend, roles: ROLE_BACKEND }
Question 1: How can I achieve that users from the same entity can authenticate differently and access certain ressources? The desired behaviour is authentication with username/password OR only apikey.
Question 2: How can I achieve, that api methods return a json if the requester is not authenticated properly, instead of returning the view for the login form? Eg. I want to return something like { 'error': 'No access' } instead of the html for the login form if someone requests /api/getSomething and of course I want to show the login form if someone requests /backend/someroute.
Every help is very much appreciated! :)
The symfony docs say:
The main job of a firewall is to configure how your users will authenticate. Will they use a login form? HTTP basic authentication? An API token? All of the above?
I think my question basically is, how can I have login form AND api token authentication at the same time.
So maybe, I need something like this: http://symfony.com/doc/current/security/guard_authentication.html#frequently-asked-questions
Question 1: When you want to authenticate users by apiKey only, then best possible solution would be implement own User provider. The solution is well decribed in the Symfony doc: http://symfony.com/doc/current/security/api_key_authentication.html
EDIT - You can have as many user providers as you want and if one fails, then another becomes to play - described here https://symfony.com/doc/current/security/multiple_user_providers.html
Down below is code for ApiKeyAuthenticator which gets the token and calls ApiKeyUserProvider to find/get user for it. In case user is found, than is provided to Symfony security. ApiKeyUserProvider needs UserRepository to user operations - I'm sure you have one, otherwise write it.
Code isn't tested, so little bit of tweaking may be necessary.
So lets get to work:
src/BackendBundle/Security/ApiKeyAuthenticator.php
namespace BackendBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
protected $httpUtils;
public function __construct(HttpUtils $httpUtils)
{
$this->httpUtils = $httpUtils;
}
public function createToken(Request $request, $providerKey)
{
//use this only if you want to limit apiKey authentication only for certain url
//$targetUrl = '/login/check';
//if (!$this->httpUtils->checkRequestPath($request, $targetUrl)) {
// return;
//}
// get an apikey from authentication request
$apiKey = $request->query->get('apikey');
// or if you want to use an "apikey" header, then do something like this:
// $apiKey = $request->headers->get('apikey');
if (!$apiKey) {
//You can return null just skip the authentication, so Symfony
// can fallback to another authentication method, if any.
return null;
//or you can return BadCredentialsException to fail the authentication
//throw new BadCredentialsException();
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new CustomUserMessageAuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// this contains information about *why* authentication failed
// use it, or return your own message
return new JsonResponse(//$exception, 401);
}
}
src/BackendBundle/Security/ApiKeyUserProvider.php
namespace BackendBundle\Security;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use BackendBundle\Entity\User;
use BackendBundle\Entity\UserORMRepository;
class ApiKeyUserProvider implements UserProviderInterface
{
private $userRepository;
public function __construct(UserORMRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function getUsernameForApiKey($apiKey)
{
//use repository method for getting user from DB by API key
$user = $this->userRepository->...
if (!$user) {
throw new UsernameNotFoundException('User with provided apikey does not exist.');
}
return $username;
}
public function loadUserByUsername($username)
{
//use repository method for getting user from DB by username
$user = $this->userRepository->...
if (!$user) {
throw new UsernameNotFoundException(sprintf('User "%s" does not exist.', $username));
}
return $user;
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Expected an instance of ..., but got "%s".', get_class($user)));
}
if (!$this->supportsClass(get_class($user))) {
throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', $this->userRepository->getClassName(), get_class($user)));
}
//use repository method for getting user from DB by ID
if (null === $reloadedUser = $this->userRepository->findUserById($user->getId())) {
throw new UsernameNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId()));
}
return $reloadedUser;
}
public function supportsClass($class)
{
$userClass = $this->userRepository->getClassName();
return ($userClass === $class || is_subclass_of($class, $userClass));
}
}
Services definition:
services:
api_key_user_provider:
class: BackendBundle\Security\ApiKeyUserProvider
apikey_authenticator:
class: BackendBundle\Security\ApiKeyAuthenticator
arguments: ["#security.http_utils"]
public: false
And finally security provider config:
providers:
chain_provider:
chain:
providers: [api_key_user_provider, db_username]
api_key_user_provider:
id: api_key_user_provider
db_username:
entity:
class: BackendBundle:User
property: username
I encourage you to study Symfony docs more, there is very good explanation for the authentication process, User entities, User providers, etc.
Question 2: You can achieve different response types for access denied event by defining own Access denied handler:
namespace BackendBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
public function handle(Request $request, AccessDeniedException $accessDeniedException)
{
$route = $request->get('_route');
if ($route == 'api')) {
return new JsonResponse($content, 403);
} elseif ($route == 'backend')) {
return new Response($content, 403);
} else {
return new Response(null, 403);
}
}
}
I am trying to authenticate a user via external api key request following this http://symfony.com/doc/current/cookbook/security/api_key_authentication.html#cookbook-security-api-key-config
What is ["#your_api_key_user_provider"] ?
If I put something like ["test"] I get an error.
[UPDATE]
This is my ApiKeyAuthenticator.php:
// src/Acme/HelloBundle/Security/ApiKeyAuthenticator.php
namespace Acme\HelloBundle\Security;
use ////
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
protected $userProvider;
public function __construct(ApiKeyUserProvider $userProvider)
{
$this->userProvider = $userProvider;
}
public function createToken(Request $request, $providerKey)
{
if (!$request->query->has('apikey')) {
throw new BadCredentialsException('No API key found');
}
return new PreAuthenticatedToken(
'anon.',
$request->query->get('apikey'),
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$apiKey = $token->getCredentials();
$username = $this->userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
throw new AuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $this->userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
}
While the user provider is this:
// src/Acme/HelloBundle/Security/ApiKeyUserProvider.php
namespace Acme\HelloBundle\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
public function getUsernameForApiKey($apiKey)
{
// Look up the username based on the token in the database, via
// an API call, or do something entirely different
$username = ...;
return $username;
}
public function loadUserByUsername($username)
{
return new User(
$username,
null,
// the roles for the user - you may choose to determine
// these dynamically somehow based on the user
array('ROLE_USER')
);
}
public function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public function supportsClass($class)
{
return 'Symfony\Component\Security\Core\User\User' === $class;
}
}
The service should be just this:
services:
# ...
apikey_authenticator:
class: Acme\SeedBundle\Security\ApiKeyAuthenticator
arguments: ["#ApiKeyUserProvider"]
But i got this error: The service "apikey_authenticator" has a dependency on a non-existent service "apikeyuserprovider".
Thanks
That is the user provider service that you should have created following this doc:
http://symfony.com/doc/current/cookbook/security/custom_provider.html
So you register your user provider as a service IE: apikey_userprovider
http://symfony.com/doc/current/cookbook/security/custom_provider.html#create-a-service-for-the-user-provider
Then pass it using ["#apikey_userprovider"]
So your Services File should look like:
parameters:
apikey_userprovider.class: Acme\HelloBundle\Security\ApiKeyUserProvider
apikey_authenticator.class: Acme\SeedBundle\Security\ApiKeyAuthenticator
services:
apikey_userprovider:
class: %apikey_userprovider.class%
apikey_authenticator:
class: %apikey_authenticator.class%
arguments: ["#apikey_userprovider"]
You need to define your user provider as a service. This is what the # operator is telling symfony to look for. Defining your classes in the parameters is just part of Symfony Coding Standards
I'm working on a custom provider that works exactly like a classical user form, however I have to give a second parameter to identify the user: a websiteId (I'm creating a dynamic website plateform).
So a username is no more unique, but the combinaison of username and websiteId it is.
I successfully created my custom authentication, the last problem I have is to get the websiteId from the domain thanks to a listener, it works, but infortunately the method that get the website id from the domain is loaded after my authentication provider, so I can't get the websiteId in time :(
I tried to change the listener priority (test 9999, 1024, 255 and 0, and negative numbers -9999, -1024, -255 etc...), in vain, it's loaded always after.
Here my code:
services.yml:
services:
# Listeners _________________
website_listener:
class: Sybio\Bundle\WebsiteBundle\Services\Listener\WebsiteListener
arguments:
- #doctrine
- #sybio.website_manager
- #translator
- %sybio.states%
tags:
- { name: kernel.event_listener, event: kernel.request, method: onDomainParse, priority: 255 }
# Security _________________
sybio_website.user_provider:
class: Sybio\Bundle\WebsiteBundle\Security\Authentication\Provider\WebsiteUserProvider
arguments: [#website_listener, #doctrine.orm.entity_manager]
My listener is "website_listener", and you can see i use it for my sybio_website.user_provider as argument.
WebsiteListener:
// ...
class WebsiteListener extends Controller
{
protected $doctrine;
protected $websiteManager;
protected $translator;
protected $websiteId;
/**
* #var array
*/
protected $entityStates;
public function __construct($doctrine, $websiteManager, $translator, $entityStates)
{
$this->doctrine = $doctrine;
$this->websiteManager = $websiteManager;
$this->translator = $translator;
$this->entityStates = $entityStates;
}
/**
* #param Event $event
*/
public function onDomainParse(Event $event)
{
$request = $event->getRequest();
$website = $this->websiteManager->findOne(array(
'domain' => $request->getHost(),
'state' => $this->entityStates['website']['activated'],
));
if (!$website) {
throw $this->createNotFoundException($this->translator->trans('page.not.found'));
}
$this->websiteId = $website->getId();
}
/**
* #param integer $websiteId
*/
public function getWebsiteId()
{
return $this->websiteId;
}
}
$websiteId is hydrated, not in time as you will see in my provider...
WebsiteUserProvider:
<?php
namespace Sybio\Bundle\WebsiteBundle\Security\Authentication\Provider;
// ...
class WebsiteUserProvider implements UserProviderInterface
{
private $em;
private $websiteId;
private $userEntity;
public function __construct($websiteListener, EntityManager $em)
{
$this->em = $em;
$this->websiteId = $websiteListener->getWebsiteId(); // Try to get the website id from my listener, but it's method onDomainParse is not called in time
$this->userEntity = 'Sybio\Bundle\CoreBundle\Entity\User';
}
public function loadUserByUsername($username)
{
// I need the websiteId here to identify the user by its username and the website:
if ($user = $this->findUserBy(array('username' => $username, 'website' => $this->websiteId))) {
return $user;
}
throw new UsernameNotFoundException(sprintf('No record found for user %s', $username));
}
// ...
}
So any idea will be appreciate ;)
I spent a lot of time to set up my authentication configuration, but now I can't get the websiteId in time, too bad :(
Thanks for your anwsers !
EDIT:
I had also other files of my authentication system to understand, I don't think I can control the provider position when loading, because they're witten in the security.yml config:
WebsiteAuthenticationProvider:
// ...
class WebsiteAuthenticationProvider extends UserAuthenticationProvider
{
private $encoderFactory;
private $userProvider;
/**
* #param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider
* #param UserCheckerInterface $userChecker
* #param $providerKey
* #param EncoderFactoryInterface $encoderFactory
* #param bool $hideUserNotFoundExceptions
*/
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, EncoderFactoryInterface $encoderFactory, $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
$this->encoderFactory = $encoderFactory;
$this->userProvider = $userProvider;
}
/**
* {#inheritdoc}
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if ($user instanceof UserInterface) {
return $user;
}
try {
$user = $this->userProvider->loadUserByUsername($username);
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}
return $user;
} catch (UsernameNotFoundException $notFound) {
throw $notFound;
} catch (\Exception $repositoryProblem) {
throw new AuthenticationServiceException($repositoryProblem->getMessage(), $token, 0, $repositoryProblem);
}
}
// ...
}
The factory:
// ...
class WebsiteFactory extends FormLoginFactory
{
public function getKey()
{
return 'website_form_login';
}
protected function getListenerId()
{
return 'security.authentication.listener.form';
}
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'security.authentication_provider.sybio_website.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('security.authentication_provider.sybio_website'))
->replaceArgument(0, new Reference($userProviderId))
->replaceArgument(2, $id)
;
return $provider;
}
}
SybioWebsiteBundle (dependency):
// ...
class SybioWebsiteBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new WebsiteFactory());
}
}
Security:
security:
firewalls:
main:
provider: website_provider
pattern: ^/
anonymous: ~
website_form_login:
login_path: /login.html
check_path: /login
logout:
path: /logout.html
target: /
providers:
website_provider:
id: sybio_website.user_provider
Firewall::onKernelRequest is registered with a priority of 8 (sf2.2). A priority of 9 should ensure that your listener is called first (works for me).
I had a similar problem, which was to create subdomain-specific "Campaign" sites within a single sf2.2 app: {campaign}.{domain} . Every User has many Campaigns and I, like you, wanted to prevent a User without the given Campaign from logging in.
My solution was to create a Doctrine filter to add my campaign criteria to every relevant query made under {campaign}.{domain}. A kernel.request listener (with priority 9!) is responsible for activating the filter before my generic user provider tries to loadUserByUsername. I use mongodb, but the idea is similar for ORM.
The best part is that I'm still using stock authentication classes. This is basically all there is to it:
config.yml:
doctrine_mongodb:
document_managers:
default:
filters:
campaign:
class: My\Filter\CampaignFilter
enabled: false
CampaignFilter.php:
class CampaignFilter extends BsonFilter
{
public function addFilterCriteria(ClassMetadata $targetMetadata)
{
$class = $targetMetadata->name;
$campaign = $this->parameters['campaign'];
$campaign = $campaign instanceof Campaign ? $campaign->getId() : $campaign;
if ($targetMetadata->hasField('campaign')) {
return array('campaign' => $this->parameters['campaign']);
}
if ($targetMetadata->hasField('campaigns')) {
return array('campaigns' => $this->parameters['campaign']);
}
return array();
}
}
My listener is declared as:
<service id="my.campaign_listener" class="My\EventListener\CampaignListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="9" />
<argument type="service" id="doctrine.odm.mongodb.document_manager" />
</service>
The listener class:
class CampaignListener
{
private $dm;
public function __construct(DocumentManager $dm)
{
$this->dm = $dm;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if ($campaign = $request->attributes->get('campaign', false)) {
$filters = $this->dm->getFilterCollection();
$filter = $filters->enable('campaign');
$filter->setParameter('campaign', $campaign);
}
}
}
'campaign' is available in the request here thanks to my routing configuration:
campaign:
resource: "#My/Controller/CampaignController.php"
type: annotation
host: "{campaign}.{domain}"
defaults:
campaign: test
domain: %domain%
requirements:
domain: %domain%
.. and %domain% is a parameter from config.yml or config_dev.yml
Like the response provide by benki07 it's a question of prority, you have to put your listener before the Firewall::onKernelRequest
Then, your listener will be called -> Firewall is call and your authentification listener are called with the webSiteId registered.
As you can see in the SecurityExtension.php The factories used do not have any sort of priority system. It just adds your factory to the end of the array, that's it.
Therefore it is impossible to put your custom authentication before that of symfony's security component.
An option may be to override the DaoAuthenticationProvider class paramater with your class. I hope that symfony2 will change from factories to a registry where you can add your custom authentication with a tag and a priority because this is not open/closed enough for me.
When overriding FOSUserBundle resetting password controller, there is a function call to "authenticateUser" method (line 104) :
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Controller/ResettingController.php#L104
....
$this->authenticateUser($user);
....
My problem is that I already override the Symfony authentication handler, and have my own logic when a user logs in.
EDIT
Here is my authentication handler :
<?php
/* ... all includes ... */
class AuthenticationHandler implements AuthenticationSuccessHandlerInterface, LogoutSuccessHandlerInterface
{
private $router;
private $container;
public function __construct(Router $router, ContainerInterface $container)
{
$this->router = $router;
$this->container = $container;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
// retrieve user and session id
$user = $token->getUser();
/* ... here I do things in database when logging in, and dont want to write it again and again ... */
// prepare redirection URL
if($targetPath = $request->getSession()->get('_security.target_path')) {
$url = $targetPath;
}
else {
$url = $this->router->generate('my_route');
}
return new RedirectResponse($url);
}
}
So, How could I call the "onAuthenticationSuccess" method from my authentication handler in the ResettingController ?
In order to avoid rewriting the same code...
Thanks for your help !
Aurel
You should call your onAuthenticationSuccess method loading it as a service. In your config.yml:
authentication_handler:
class: Acme\Bundle\Service\AuthenticationHandler
arguments:
container: "#service_container"
And then, call it in the authenticateUser function:
protected function authenticateUser(UserInterface $user) {
try {
$this->container->get('fos_user.user_checker')->checkPostAuth($user);
} catch (AccountStatusException $e) {
// Don't authenticate locked, disabled or expired users
return;
}
$providerKey = $this->container->getParameter('fos_user.firewall_name');
$token = new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
$this->container->get('security.context')->setToken($token);
$request = $this->container->get('request');
$this->container->get('authentication_handler')->onAuthenticationSuccess($request, $token);
}
this do the trick and pass through your custom auth handler. More info.