I have setup a firewall in Silex as follows:
$this -> register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'login' => array(
'pattern' => '^/admin/login$'
),
'admin' => array(
'pattern' => '^/admin.*$',
'form' => array(
'login_path' => '/admin/login',
'check_path' => '/admin/security/validate',
'default_target_path' => "/admin",
'always_use_default_target_path' => true
),
'logout' => array(
'logout_path' => '/admin/security/logout'
),
'users' => $app -> share(function () use ($app) {
return new \Turtle\Providers\UserProvider($app);
})
)
),
'security.access_rules' => array(
array('^/admin.*$', 'ROLE_ADMIN')
)
));
This works in that when I hit a page in the 'admin' area I get redirected to my login page. However I have started to do some authorization in my custom AuththenticationSuccess handler. I want to use the built in method determineTargeUrl to redirect on success but it keeps redirecting to '/'.
After some debugging I have found that the options in the object that the method uses has the following:
array (size=5)
'always_use_default_target_path' => boolean false
'default_target_path' => string '/' (length=1)
'login_path' => string '/login' (length=6)
'target_path_parameter' => string '_target_path' (length=12)
'use_referer' => boolean false
Clearly this is not what I have set in my firewall. It is my understanding that this should match what it is in the firewall that I have used when accessing the system. The URL I used was 'http://localhost/admin'.
So how do I make it so that the options I have set in my firewall appear in the object so that I can use the determineTargetUrl?
Thanks lots, Russell
It looks like your problem is that your login route is inside your secured area. You've defined your access rule as ^/admin.*$. This means that any route starting with /admin requires ROLE_ADMIN including your login route. To fix this you need to remove security from your login route.
Add a new access rule above your admin rule.
'security.access_rules' => array(
array('^/admin/login$', 'IS_AUTHENTICATED_ANONYMOUSLY'),
array('^/admin.*$', 'ROLE_ADMIN')
)
Edit: After reading your question again, I may have misunderstood you. it sounds like you can successfully log in but are redirected to the wrong place after a successful login. Is that correct? If so I will remove this answer.
This was my mistake. I am using custom AuthenticationSuccess and AuthenticationFailure handlers and i neglected to pass any options into them when I was declaring them:
$app['security.authentication.success_handler.admin'] = $app -> share(function() use ($app) {
return new AuthenticationSuccessHandler($app['security.http_utils'], array(), $app);
});
$app['security.authentication.failure_handler.admin'] = $app -> share(function() use ($app) {
return new AuthenticationFailureHandler($app['kernel'], $app['security.http_utils'], array(), $app);
});
So the options array that is used in determineTargetUrl on authentication success was empty and thus had default values.
By adding an array of options to the AuthenticationSuccessHandler it works. This is OK as each custom handler is linked to a different firewall.
Related
I searched for this kind of connection but all the bundles do not work with symfony security 2.7 (Composer says packages problems) and symfony 3.0.
Note : I actually use Silex Framework.
I want to return a true or false response if the user is connected to the active directory.
I have this code for tests :
$app->register(new Silex\Provider\SecurityServiceProvider(), array(
'security.firewalls' => array(
'login' => array(
'pattern' => '^/login$',
),
'secured' => array(
'pattern' => '^/',
'anonymous' => false,
'logout' => true,
'form' => array('login_path' => '/login', 'check_path' => '/login_check'),
'users' => array(
// raw password is foo
'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
),
),
),
));
The login with admin & foo worked but that's not what I want.
I want to replace this part of code :
'users' => array(
// raw password is foo
'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
),
Or any others solutions to replace the value who allows the user to bypass the firewall and enter in the application.
Note :
I use adLDAP plugin to test the connection with the Active Directory (It's works perfectly).
Try to use LdapUserProvider for it. Remove 'users' => array(...) from secured config and add user provider definition for this zone.
$app['security.user_provider.secured'] = $app->share(function($app) {
return new \Symfony\Component\Security\Core\User\LdapUserProvider(
new \Symfony\Component\Ldap\LdapClient('ldap.server.com'),
'baseDn', // ex.: dc=example,dc=com
'searchDn' // ex.: CN={username},DC=example,DC=com
);
});
Or you can write your own user provider based on LdapUserProvider and adLDAP. Something like this:
namespace My\Namespace;
use Symfony\Component\Security\Core\User\LdapUserProvider;
use Symfony\Component\Security\Core\User\User;
class adLdapUserProvider extends LdapUserProvider
{
public function loadUserByUsername($username)
{
// you code, that returns user from ldap server
// $this->ldap - is object of adLDAP
$user = $this->ldap->user()->info($username);
return $this->loadUser($username, $user);
}
}
and change user provider definition
$app['security.user_provider.secured'] = $app->share(function($app) {
return new \My\Namespace\adLdapUserProvider(
new \adLDAP\adLDAP(...)
);
});
I am using the Silex / Symfony security service and try to implement a automatic login when the specific parameters are passed in the request query.
I've looked into the modules and also search on the internet for a solution and always found something like the following:
$user = (new \Portal\UserProvider($app['databases']['read']))->loadUserByUsername($subscriber_id);
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, $user->getPassword(), 'secured', $user->getRoles());
$app['security.token_storage']->setToken($token);
Unfortunately, this does not work for my app. I don't know whats wrong but the security module keeps redirecting me to /login/ as specified in the registration process:
/**
* Registers the security firewall.
*/
private function registerSecurity()
{
$this->register(new \Silex\Provider\SecurityServiceProvider(), array(
'security.firewalls' => array(
'login' => array(
'pattern' => '^/(login/|terms|imprint|animation|error)',
),
'secured' => array(
'pattern' => '^/',
'form' => array(
'login_path' => '/login/',
'check_path' => '/login_check'
),
'logout' => array(
'logout_path' => '/logout'
),
'users' => $this->share(function () {
return new \Portal\UserProvider($this['databases']['read']);
}),
),
'unsecured' => array(
'anonymous' => true
),
),
'security.encoder.digest' => $this->share(function () {
return new \Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder('sha1', false, 1);
}),
'security.access_rules' => array(
array('^/login', 'ROLE_GUEST'),
),
'security.role_hierarchy' => $this->share(function () {
return array();
})
));
$this->boot();
}
Is there anything I have to consider about
reloading
order of registering the SecurityServiceProvider, SessionServiceProvider
this manual token setting
?
You're using the 'form' authentication provider, but this won't work (or maybe I'm not understanding you correctly?). In order to be able to:
try to implement a automatic login when the specific parameters are passed in the request query
You need to hook into the Security service. In order to do that you have to create a Listener and register it. You'll also need a Provider
This is not an easy path as the security component works with many concepts.
You can see a working example in this repo (which implements an OAuth service)
If your security flow is easy and you don't need roles, you can just use a before middleware (and forget about the security component) like so:
<?php
$app->before(function (Request $request, Application $app) {
$session = $request->getSession();
if (false === $session->get('logged', false)) {
if (null !== $request->get('blah', null)) {
$session->set('logged', true);
}
else {
return new RedirectResponse('/login-url');
}
}
});
You could use Silex's guard. It works well with get Request. And standard form could be use as complement.
In your secured Security.firwall, add the guard parameter :
"guard" => array ("authenticator" => array("app.myauthenticator") )
And create your custom class, to validate login.
Just read the Silex cookbook.
I am using Silex 2.0 (I know - it's development version and not fully released yet) along with CNAM's JWT security provider (see: https://github.com/cnam/security-jwt-service-provider) to write an API for an open source application I am writing.
In short, there are three types of users that I care about:
Sitewide admins (ROLE_ADMIN) that have complete access
Commissioners (ROLE_COMMISH) who create objects they own, and can edit their own objects
Anonymous users who access read-only information.
As such, there are three sections of routes that go along with these "roles":
/admin/* where administrators can perform their uber actions
/commish/* where commissioners or admins can perform their actions on their objects
/* where all users can read information
The issue that I've come across is that while I can setup 3 firewalls, one for each, there are times in the 3rd route category (GET /object/1 for instance) where it needs to be accessibly anonymously, but if the user provides a valid JWT token, I need to access that user in order to perform some additional logic on the data I hand back in the response.
As I have it setup currently (more on my config below), it's all-or-nothing: I either restrict an entire firewall to only authenticated users with a certain role, or I open it up to anonymous users (and therefore cannot view user information).
Is it possible to have a route that anyone can hit, but logged in users can also be seen?
Current security configuration:
$app['users'] = function () use ($app) {
return new UserProvider($app);
};
$app['security.jwt'] = [
'secret_key' => AUTH_KEY,
'life_time' => 86400,
'algorithm' => ['HS256'],
'options' => [
'header_name' => 'X-Access-Token'
]
];
$app['security.firewalls'] = array(
'login' => [
'pattern' => 'login|register|verify|lostPassword|resetPassword',
'anonymous' => true,
],
'admin' => array(
'pattern' => '^/admin',
'logout' => array('logout_path' => '/logout'),
'users' => $app['users'],
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true,
)
),
'commish' => array(
'pattern' => '^/commish',
'logout' => array('logout_path' => '/logout'),
'users' => $app['users'],
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true,
)
)
);
$app['security.role_hierarchy'] = array(
'ROLE_ADMIN' => array('ROLE_MANAGER'),
);
$app->register(new Silex\Provider\SecurityServiceProvider());
$app->register(new Silex\Provider\SecurityJWTServiceProvider());
Additionally, I've attempted another approach where I match all routes under a single firewall, but then protect certain ones by using securty.access_rules configuration, but it does not work. An example of what I've tried:
$app['security.firewalls'] = array(
'api' => array(
'pattern' => '^/',
'logout' => array('logout_path' => '/logout'),
'anonymous' => true,
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true
)
)
);
$app['security.access_rules'] = array(
array('^/admin', 'ROLE_ADMIN'),
array('^/commish', 'ROLE_MANAGER'),
array('^/', 'IS_AUTHENTICATED_ANONYMOUSLY')
);
You can use $app['security.jwt.encoder'] to decode jwt and either create a custom trait and extending the route object or using midddlewareeeither on the route level or an easier way would be to use a middleware on the application level. I had similar issue and this is how i solved it, something like below
ex.
$app->before(function (Request $request, Application $app) {
$request->decodedJWT = $app['security.jwt.encoder']->
decode($request->headers->get('X-Access-Token'));
});
and then you can access the decoded jwt form any route by doing this
$app->get('/object/1', function(Request $request) {
$decodedJWT = $request->decodedJWT;
// do whatever logic you need here
})
So: so far I have not found this to be possible through the "normal" way, which is disappointing. I will not mark what I detail below as the "answer" for a few days, hoping that someone can chime in and offer a better, more "official" way to solve the dilemma.
TL;DR: I manually check the request headers for the access token string, then decode the token using the JWT classes in order to load the user account in routes outside of the firewall. It's incredibly hacky, it feels downright dirty, but it's the only solution to the issue that I see at the moment.
Technical Details: First, you must acquire the token value from the request header. Your controller method will have been handed a Symfony\Component\HttpFoundation\Request object, from which you can access $request->headers->get('X-Access-Token'). In most instances the user will not be authenticated, so this will be empty, and you can return null.
If not empty, you must then use Silex's instance of JWTEncoder to decode the token contents, create a new token instance of JWTToken, set the context to the decoded value from the encoder, and finally you can access the username property from said token - which can then be used to grab the corresponding user record. An example of what I came up with:
$request_token = $request->headers->get('X-Access-Token','');
if(empty($request_token)) {
return null;
}
try {
$decoded = $app['security.jwt.encoder']->decode($request_token);
$token = new \Silex\Component\Security\Http\Token\JWTToken();
$token->setTokenContext($decoded);
$userName = $token->getTokenContext()->name;
//Here, you'd use whatever "load by username" function you have at your disposal
}catch(\Exception $ex) {
return null;
}
And obviously, any code calling this function would need to know that because the request is outside of the firewall, there is zero guarantee that a user will be returned (hence the hacky try-catch that silences exceptions by just returning null).
Edit: I've updated the code here to use Silex's built-in DI container (provided by Pimple) so there's no need to create a new instance of the JWT encoder by hand. I'm also marking #user5117342 's answer as the correct one, as using some sort of Silex middleware approach is far more robust.
Edit (April 2016): Using the updated cnam/security-jwt-service 2.1.0 along with symfony/security 2.8, there's a slight update that makes the code above a little simpler:
$request_token = $request->headers->get('X-Access-Token','');
if(empty($request_token)) {
return null;
}
try {
$decodedToken = $app['security.jwt.encoder']->decode($request_token);
$userName = $decodedToken->name;
//Here, you'd use whatever "load by username" function you have at your disposal
}catch(\Exception $ex) {
return null;
}
The issue with the newer dependencies is that the JWTToken constructor requires 3 parameters which are difficult to obtain in most service layers, not to mention is quite out of place. As I was updating my Composer dependencies, I ended up finding out that I didn't actually need to create a JWTToken in order to get the username I needed.
Of course, it's to be noted I'm only using this method on public (anonymous) API routes to provide some niceties to users who are logged in - my app doesn't deal with sensitive data so I'm not overly concerned with this avenue outside of the firewalls. At worst a black hat user would end up seeing non-sensitive data that they normally wouldn't, but that's it. So YMMV.
Your are must be use regular expression e.g.
$app['security.firewalls'] = array(
'login' => [
'pattern' => 'login|register|oauth',
'anonymous' => true,
],
'secured' => array(
'pattern' => '^/api|/admin|/manager',
'logout' => array('logout_path' => '/logout'),
'users' => $app['users'],
'jwt' => array(
'use_forward' => true,
'require_previous_session' => false,
'stateless' => true,
)
),
);
i would like to match all requests where user is unlogged to controller Admin\Controller\Sign and action in. I wrote this code in onBootstrap() method in Module.php file :
if (!$authService->hasIdentity()) {
$routeMatch = new RouteMatch(
array(
'controller' => 'Admin\Controller\Sign',
'action' => 'in'
)
);
$event->setRouteMatch($routeMatch);
}
I don't get any errors, but code doesn't work, why?
The problem here is that the application route event (MvcEvent::EVENT_ROUTE) is triggered after the (MvcEvent::EVENT_BOOTSTRAP).
Which means even if you're setting the route match at the bootstrap level, the application is going to override it with the route match of the request after the MvcEvent::EVENT_ROUTE.
If you want to avoid this overriding you need to add a listener for the route event with a very low priority to make sure it will not be overridden:
$e->getApplication()->getEventManager()->attach(MvcEvent::EVENT_ROUTE, array($this, 'onRouteEvent'), -1000000);
Note : the onRouteEvent would be the method of your Module class that handles the route event (similar to your code).
If you want to short-circuit your application running at the bootstrap level, what you can do is to send the headers with redirection code to the client:
//get the url of the login page (assuming it has route name 'login')
$url = $e->getRouter()->assemble(array(), array('name' => 'login'));
$response=$e->getResponse();
$response->getHeaders()->addHeaderLine('Location', $url);
$response->setStatusCode(302);
$response->sendHeaders();
add a route entry sign_in as below in the routes section of the module.config.php under admin module
'sign_in' => array(
'type' => 'Segment',
'options' => array(
'route' => '/admin/sign/in',
'defaults' => array(
'controller' => 'sign',
'action' => 'in',
),
),
),
and call the route in the controller like this
$this->redirect()->toRoute('sign_in');
I have some issues with the SecurityServiceProvider of Silex.
Basically what I want is the following structure:
/admin/ --> The administration page that is restricted to some users
(can have multiple suppages e.g. /admin/users and /admin/projects)
/admin/login --> The page that visitors can use to login to the
administration page
/admin/logout --> The page visitors see when they logged off from the
administration page
To implement this, I wrote the following code:
<?php
require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$app = new Silex\Application();
$app['debug'] = true;
$app->register(new Silex\Provider\SecurityServiceProvider());
$app->register(new Silex\Provider\TwigServiceProvider(), array(
'twig.path' => __DIR__.'/views',
));
$app->register(new Silex\Provider\UrlGeneratorServiceProvider());
$app->register(new Silex\Provider\SessionServiceProvider());
$app['security.firewalls'] = array(
'login' => array(
'pattern' => '^/admin/login$'
),
'logout' => array(
'pattern' => '^/admin/logout$'
),
'admin' => array(
'pattern' => '^/admin/',
'form' => array('login_path' => '/admin/login', 'check_path'
=> '/admin/login_check'),
'users' => array(
'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC
+GsReLf569mSKDsfods6LYQ8t
+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='), // PW is foo
),
'logout' => array('logout_path' => '/admin/logout')
),
);
$app->get('/admin/login', function(Request $request) use ($app) {
return $app['twig']->render('login.html', array(
'error' => $app['security.last_error']($request),
'last_username' => $app['session']-
>get('_security.last_username'),
));
});
$app->get('/admin/logout', function(Request $request) use ($app) {
return $app['twig']->render('logout.html', array());
});
$app->get('/admin/', function () use ($app) {
return $app['twig']->render('admin.html', array());
});
$app->run();
?>
Now, what happens is that when I visit the page "/admin/" I always get
redirected to "/login" which is good since I am not authenticated but
it should be "/admin"login" as the "login_path" parameter in my
configuration indicates... what am I doing wrong or could this be a
bug in the SecurityServiceProvider?
The only thing first comes to my mind is that the manual tells you that:
The login_path path must always be defined outside the secured area (or if it is in the secured area, the anonymous authentication mechanism must be enabled)
See here:
Silex doc on security with form
I hope it helps you solve a basic flaw and you can carry on.