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);
}
}
}
Related
After a good amount of time, I found the solution through an authentication plugin that is easy to install via composer with the command:
composer require "cakephp/authentication:^2.0"
After that, just generate the crud via terminal and check them to send json type data. It can be done as follows:
$this->response
->withType('application/json')
->withStatus(200)
->withStringBody(json_encode($dataOrMessage));
Add the function in entity to encrypt the password that the user registers:
protected function _setPassword(string $password) : ?string {
if (strlen($password) > 0) {
return (new DefaultPasswordHasher())->hash($password);
}
}
Implementing the AuthenticationServiceProviderInterface interface in the Application class and adding its requirements, in addition to the login and logout functions of the aforementioned plugin, you can view here
The login function will look something like this:
public function login() {
if ($this->request->is('post')) {
$result = $this->Authentication->getResult();
if ($result && $result->isValid()) {
return $this->response
->withType('application/json')
->withStatus(200)
->withStringBody(json_encode($result->getData()));
} else {
return $this->response
->withType('application/json')
->withStatus(401)
->withStringBody(json_encode(['message' => 'User or password incorrect!']));
}
}
}
There is not only this solution, but it was the one I found and that I found to be easier to implement.
In my current application, I am using Service Stack with JWT's for security. Security has been implemented and works perfectly. Trouble is, I would like to secure one route differently from the others. There is a document the logged in user retrieves, I want to make sure the document they are retrieving is theirs and not someone else's. It is very sensitive data. I would like to secure it differently because something like PostMan could be used with a valid token to retrieve any document, I want to prevent this. The users id is in the token, I would like to match it against the document that is being retrieved if possible. The current security is implemented like so:
public class AppHost: AppHostBase
{
public override void Configure(Funq.Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new JsonWebTokenAuthProvider("myKey", "myAudience"),
}));
}
}
JsonWebTokenAuthProvider is a custom class where security was implemented, this all works perfectly. Here is the code:
public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request)
{
// first validate the token, then get roles from session
string header = request.oauth_token;
// if no auth header, 401
if (string.IsNullOrEmpty(header))
{
throw HttpError.Unauthorized(MissingAuthHeader);
}
string[] headerData = header.Split(' ');
// if header is missing bearer portion, 401
if (!string.Equals(headerData[0], "BEARER", StringComparison.OrdinalIgnoreCase))
{
throw HttpError.Unauthorized(InvalidAuthHeader);
}
// swap - and _ with their Base64 string equivalents
string secret = SymmetricKey.Replace('-', '+').Replace('_', '/');
string token = headerData[1].Replace("\"", "");
// set current principal to the validated token principal
Thread.CurrentPrincipal = JsonWebToken.ValidateToken(token, secret, Audience, true, Issuer);
string lanId = GetLanID(Thread.CurrentPrincipal.Identity.Name);
string proxyAsLanId = request.Meta.ContainsKey(META_PROXYID) ? request.Meta[META_PROXYID] : null;
if (HttpContext.Current != null)
{
// set the current request's user the the decoded principal
HttpContext.Current.User = Thread.CurrentPrincipal;
}
// set the session's username to the logged in user
session.UserName = Thread.CurrentPrincipal.Identity.Name;
session.Roles = GetApplicableRoles(lanId, proxyAsLanId);
authService.Request.SetItem("lanID", lanId);
authService.Request.SetItem("proxyAsLanId", proxyAsLanId);
return OnAuthenticated(authService, session, null, null);
}
I looked up RequestFilterAttribute found here, but I do not think that is what I want. Ideally, if the check fails I would like to return a 401 (unauthorized) if possible.
What is the best way to do this?
If you just want to handle one route differently than you can just add the validation in your single Service, e.g:
public object Any(MyRequest dto)
{
var lanId = base.Request.GetItem("lanId");
if (!MyIsValid(lanId))
throw HttpError.Unauthorized("Custom Auth Validation failed");
}
You could do the same in a RequestFilter, e.g:
public class CustomAuthValidationAttribute : RequestFilterAttribute
{
public override void Execute(IRequest req, IResponse res, object responseDto)
{
var lanId = req.GetItem("lanId");
if (!MyIsValid(lanId))
{
res.StatusCode = (int) HttpStatusCode.Unauthorized;
res.StatusDescription = "Custom Auth Validation failed";
res.EndRequest();
}
}
}
And apply it to a single Service:
[CustomAuthValidation]
public object Any(MyRequest dto)
{
//...
}
Or a collection of Services, e.g:
[CustomAuthValidation]
public class MyAuthServices : Service
{
public object Any(MyRequest1 dto)
{
//...
}
public object Any(MyRequest2 dto)
{
//...
}
}
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 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 !
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.