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,
)
),
);
Related
I'm using this JWTAuth adapter to use JWT authentication instead of cookie-based auth in my CakePHP 2.8 app. It works great, except for one hitch:
Normally for one of my REST endpoints, I can use $this->Auth->user("id") to get the currently logged-in users' ID.
When I try to make a controller action accessible to non-members using $this->Auth->allow(), a problem occurs. If I do this, using $this->Auth->loggedIn() in the controller returns false, meaning I can not add additional logic for logged-in users.
When using standard cookie auth:
$this->Auth->user('id') is available in
Controller::beforeFilter().
$this->Auth->loggedIn() is true in
Controller::beforeFilter().
$this->Auth->user('id') is available in controller actions, public
and members-only.
$this->Auth->loggedIn() is true in controller actions, public
and members-only.
When using JWT auth:
$this->Auth->user('id') is null in Controller::beforeFilter().
$this->Auth->loggedIn() is false in
Controller::beforeFilter().
$this->Auth->user('id') is available in members-only controller actions, and null in public controller actions.
$this->Auth->loggedIn() is true in members-only controller actions, and false in public controller actions.
Is there any way I can get Auth to include information returned by the JWTAuth component on actions that have been made public by $this->Auth->allow()?
Example controller here:
public function visible(){
// This will always be false, even if a valid JWT token is sent
$this->set("loggedIn", $this->Auth->loggedIn());
}
public function members_only(){
// This will be unavailable if not logged in, and a true if logged in
$this->set("loggedIn", $this->Auth->loggedIn());
}
public function beforeFilter($options = array()){
parent::beforeFilter();
$this->Auth->allow("visible");
}
And for reference, my AppController::components array;
public $components = array(
'DebugKit.Toolbar',
'Auth' => array(
'authorize' => array(
'Actions' => array(
'actionPath' => 'controllers'
),
),
'authenticate' => array(
'Form' => array(
'fields' => array('username' => 'email'),
'contain' => array(
'UserProfile',
)
),
'JwtAuth.JwtToken' => array(
'fields' => array(
'username' => 'email',
'token' => 'password',
),
'header' => 'AuthToken',
'userModel' => 'User',
),
),
'unauthorizedRedirect' => false
),
"Acl",
"RequestHandler",
"Session"
);
For stateless adapters authentication process is triggerd in AuthComponent::startup(). Component's startup() methods are run after Controller::beforeFilter(), that is why AuthComponent::user() doesn't return any info.
For other adapters when user is authenticated the identity info is stored in session. Getting that info doesn't require any authentication process which is why AuthComponent::user() give you the user info in case of standard cookie based auth.
I'm a bit late, but I found a solution to this problem, but (warning!) it involved updating the code for AuthComponent.
I took a copy of Lib/Controller/Component/AuthComponent.php and placed it under app/Controller/Component/AuthComponent.php.
I then added one line to the file:
public function startup(Controller $controller) {
$methods = array_flip(array_map('strtolower', $controller->methods));
$action = strtolower($controller->request->params['action']);
// One-line modification from the ordinary CakePHP file.
// This lets us have info on the logged-in user in public actions when using stateless auth.
$this->_getUser();
Voila, you can now access user info on the server while accessing a non-protected controller function with stateless auth!
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'm new to Silex and Symfony and decided to use Silex to build a (really) small app which uses an authentication. The auth process is working. I'm currently working on a little setup page which allows the user to define his username and password. The auth looks like this:
$app['security.firewalls'] = array(
'admin' => array(
'pattern' => '/private',
'form' => array('login_path' => '/login', 'check_path' => '/private/login_check'),
'logout' => array('logout_path' => '/private/logout', 'invalidate_session' => true),
'users' => array(
$username => array('ROLE_ADMIN', $passwd)
)
)
);
$app->register(new Silex\Provider\SecurityServiceProvider());
No other type of user is defined appart from admin. The setup is meant to be run once by an admin.
I've tried customizing the username and the password the following ways:
First thing (this is common to everything I tried)):
// Default password and username
$username='admin'; $passwd='5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==';`
The following didn't work (no magical update):
`$app->post('/setup', function(Request $request) use ($app, $username, $passwd) {
$username = $request->get('username');
$passwd= $request->get('passwd');
});
so I tried something like:
$app->post('/setup', function(Request $request) use ($app) {
$username = $request->get('username');
$passwd= $request->get('passwd');
$app['security.firewalls']['admin']['users'] = array($username => array('ROLE_ADMIN', $passwd));
$app->register(new Silex\Provider\SecurityServiceProvider());
});
with and without registering a new SecurityServiceProvider, and redifining the "users" part of the array. Merging didn't help either.
I'm a bit out of idea here :S How can I perform that?
Dynamic Way
You are not saving your User anywhere. I assume you define $username and $passwd before you define $app['security.firewalls'], otherwise that should already throw an error.
What you need is an User Provider. Right now you are not saving the new user(s), and you are trying to just re-define your two static variables - without checking the given values if I may add - and that is wrong in multiple ways :)
Either you dive in a little bit and check out how to Define a Custom User Provider in Silex and if you have special requirements to an User-Object also probably how to create an User-Entity, or you use some already written user provider for the Silex security service, like:
Silex-SimpleUser by Jason Grimes
Static Way
If you don't want to dynamically add/edit/delete users or you just have a handful of users, you can always just add your static users to the users array:
'users' => array(
'admin1' => array('ROLE_ADMIN', 'encodedAndSaltedPW1'),
'admin2' => array('ROLE_ADMIN', 'encodedAndSaltedPW2'),
'admin3' => array('ROLE_ADMIN', 'encodedAndSaltedPW3'),
)
In this case you would need to encode and salt the PWs like described in the docs:
// find the encoder for a UserInterface instance
$encoder = $app['security.encoder_factory']->getEncoder($user);
// compute the encoded password for foo
$password = $encoder->encodePassword('foo', $user->getSalt());
Also see the docs for the SecurityServiceProvider for more information.
Your Way: Single-User-Provider
After your comments I understand your needs better.
You could of course have an own ordinary "Single-User-Provider" - this could go something like this:
$app->post('/setup', function(Request $request) use ($app) {
$username = $request->get('username');
$passwd= $request->get('passwd');
$file = 'config.json';
$config = array($username => array('ROLE_ADMIN', $passwd));
file_put_contents($file, json_encode($config));
return $app->redirect($app['url_generator']->generate('login'));
});
Be aware that a json-file, without proper permissions, could be read by anyone - so make sure to put the encoded/salted password in it, by no means the readable one.
And then you would need to read the config.json into your Firewall-Configuration:
$app['security.firewalls'] = array(
'admin' => array(
'pattern' => '/private',
'form' => array('login_path' => '/login', 'check_path' => '/private/login_check'),
'logout' => array('logout_path' => '/private/logout', 'invalidate_session' => true),
'users' => json_decode(file_get_contents('config.json'))
)
);
Please notice, that this code is untested and should more likely give you an idea. I hope this helps.
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.
In cakephp 2.5.4, with $this->Auth->user() I can access to user object
I want when user logged in, set some user detail in user object and with $this->Auth->user() function, get all info in all controller
$user = $this->Auth->user();
$this->loadModel('Resource');
$resource = $this->Resource->find('threaded', array('order' => 'index'));
// here, i want add $resource to user object
How can I do this?
Try this, untested but should work.
$this->Session->write($this->Auth->sessionKey, array_merge(
$resource,
$this->Auth->user()
));
It is unlikely that the session key changes in most apps but it can happen. I don't consider it as good practice to hardcode the key everywhere like most people do.
Also keep in mind that this won't work with state less authentication systems!
You can also contain additional data when configuring the Auth adapters. Read this section of the manual. Taken from the manual and added the contain:
public $components = array(
'Auth' => array(
'authenticate' => array(
'Form' => array(
'fields' => array('username' => 'email'),
'contain' => array('Foo', 'Bar')
)
)
)
);