Add a user to the firewalls' user list in Silex - php

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.

Related

Silex / Symfony programmatically login

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.

Silex: Get Authenticated User Information on Routes Outside of Firewall

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,
)
),
);

cakephp - add detail to static user object

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')
)
)
)
);

How to configure CakePHP's $this->Auth->login() to use custom password hasher

CakePHP v.2.4...
I'm following this documentation trying to set up the Auth component to use my custom password hashing class:
App::uses('PHPassPasswordHasher', 'Controller/Component/Auth');
class AppController extends Controller {
// auth needed stuff
public $components = array(
'Session',
'Cookie',
'Auth' => array(
'authenticate' => array(
'Form' => array(
'fields' => array('username'=>'email', 'password'=>'password'),
'passwordHasher' => 'PHPass'
)
),
Inside my UsersController::login() I debug the return from $this->Auth->login(); and it always returns false, even when I submit the correct email / password.
(NOTE: It looks strange to me that the login() takes no parameters, but the docs seem to imply that it looks into the the request data automatically. And this would make sense if my configurations aren't correctly causing it to check the User.email field instead username.)
The post data from the submitted login form looks like this:
array(
'User' => array(
'password' => '*****',
'email' => 'whatever#example.com'
)
)
What am I missing?
Update2
I'm starting to suspect that the default hashing algorithm is getting used instead of my custom class. I tried to match the examples in the docs but they're quite vague on how to do this.
Here's the contents of app/Controller/Component/Auth/PHPassPasswordHasher.php
<?php
App::import('Vendor', 'PHPass/class-phpass'); //<--this exists and defines PasswordHash class
class PHPassPasswordHasher extends AbstractPasswordHasher {
public function hash($password) {
$hasher = new new PasswordHash( 8, true );
return $hasher->HashPassword($password);
}
public function check($password, $hashedPassword) {
debug('PHPassHasher'); die('Using custom hasher'); //<--THIS NEVER HAPPENS!
$hasher = new new PasswordHash( 8, true );
return $hasher->CheckPassword($password, $hashedPassword);
}
}
AHA! The debug() never appears... so I'm pretty sure the problem is with my custom hasher configuration(s).
Update3
Additional clue: I experimented by setting various default hashing algorithms (Ex: "Simple", "Blowfish") and creating users. The hashes which show up in the DB are all the same which tells me that my config settings are getting ignored completely.
Update4
I debugged $this->settings inside the constructor of /lib/Cake/Controller/Component/Auth/BaseAuthenticate.php and my custom hasher settings are in there:
array(
'fields' => array(
'password' => 'password',
'username' => 'email'
),
'userModel' => 'User',
'scope' => array(),
'recursive' => (int) 0,
'contain' => null,
'passwordHasher' => 'PHPass'
)
You need to rename your password hasher class to have the suffix "PasswordHasher", and only provide the non-suffixed name in the 'className' argument.
eg:
<?php
App::import('Vendor', 'PHPass/class-phpass'); //<--this exists and defines PasswordHash class
class PHPassHasherPasswordHasher extends AbstractPasswordHasher {
// functions
}
The example from the docs sets the classname to 'Simple', which then loads 'SimplePasswordHasher'.
You might find that having a name of PHPassHasherPasswordHasher is a bit silly, it's up to you what you want to call it. Perhaps PHPassPasswordHasher might be a bit more appropriate (and then use the classname argument 'PHPass').
EDIT: It seems as if Cake has issues when multiple capital letters are used one after the other (eg. PHPass), so the right way to do it is to change the password hasher class to the following:
<?php
App::import('Vendor', 'PHPass/class-phpass'); //<--this exists and defines PasswordHash class
class PhpassPasswordHasher extends AbstractPasswordHasher {
// functions
}
... and make sure the filename matches the classname: PhpassPasswordHasher.php.
Thanks to SDP for the discussion, I learnt something today!
According to the docs:
To configure different fields for user in $components array:
// Pass settings in $components array
public $components = array(
'Auth' => array(
'authenticate' => array(
'Form' => array(
'fields' => array(
'username' => 'email',
'password' => 'password'
)
)
)
)
);
Source
I finally got this working. We were on the right track by renaming the file/class to comply with Cake conventions. I had to go one step further and change the capitalization as well:
PHPassPasswordHasher.php --> PhpassPasswordHasher.php
class PHPassPasswordHasher... --> class PhpassPasswordHasher...
Phew!
ps: Many many thanks to #Ben Hitchcock for support on this.

How to use Auth in cakephp using mongodb

I have a mongo db structure for users with "username" and "password". I am trying to use the Auth in cakephp login but it seems like its not working for me. I tried removing the $this->data but still it did not work.
My password is hashed using Security::hash($this->data['User']['password'])
if(!empty($this->data))
{
if($this->Auth->login($this->data))
{
echo "yes";
}
else{
echo "no";
}
}
In my app controller I have this:
public $components = array('DebugKit.Toolbar', 'Session', 'Auth' => array(
'loginAction' => array(
'controller' => 'pages',
'action' => 'home'
),
'authenticate' => array(
'Form' => array(
'fields' => array('username' => 'username', 'password' => 'password')
)
)
));
Here is the result when I debug the login method:
array(
'User' => array(
'password' => '*****',
'username' => 'test#test.com',
'remember' => '0',
'auto_login' => '0'
)
)
I don't know why I cannot use Auth with mongodb. Thanks for the help in advance.
EDIT:
When i tried and take away the layout, it shows me a query at the bottom of the page saying:
db.users.find( {"username":"test#test.com","password":"2fdf49ffc396453960802df8fc2417655d1e8fca"}, [] ).sort( [] ).limit( 1 ).skip( 0 )
The hashed value of the password that I inputted from the form is different from the hash value that is being queried. The hashed value should be "a2374c309ab7823dcd9b4e21dae7511f7a9c7ec5". Why is it that cakephp is converting the password into another hash value?
There are two ways of using $this->Auth->login(). The CakePHP API documentation explains it:
If a $user is provided that data will be stored as the logged in user. If $user is empty or not specified, the request will be used to identify a user.
The manual also mentions:
In 2.0 $this->Auth->login($this->request->data) will log the user in with whatever data is posted ...
So for the login method of the users controller you shouldn't pass anything:
if($this->Auth->login()) {
// user is now logged in
}
Should you need to manually login a user you can pass the user data as an array:
if($this->Auth->login($this->request->data['User'])) {
// user is now logged in
}
Where $this->request->data['User'] is something like:
array(
'id' => 1,
'username' => 'admin',
'password' => '1234',
);
Note: In both cases you don't need to hash the password as it is done automatically.
I was able to find out the answer. Its because cakephp is automatically hashing the password when searching in the database.
The problem that I had was when I was saving the users' password, I am was using
Security::hash($this->data['User']['password'])
I should have used this one instead:
AuthComponent::password($this->data['User']['password'])
Thank you for all the help especially to #xgalvin

Categories