Silex: get information about authenticated users on routes outside the firewall

I am using Silex 2.0 (I know this is a development version and not fully released yet) along with the CNAM JWT security provider (see: https://github.com/cnam/security-jwt-service-provider ) to write the API for an open source application I'm writing.

In short, I am interested in three types of users:

  • Sitewide ( ROLE_ADMIN

    ) Administrators with Full Control
  • Commissioners ( ROLE_COMMISH

    ) who create the objects they own and can edit their own objects
  • Anonymous users who have read-only access to information.

Thus, there are three sections of routes that are combined with these "roles":

  • /admin/*

    where admins can perform their uber actions
  • /commish/*

    where commissioners or admins can perform their actions on their objects.
  • /*

    where all users can read information

The problem I am facing is that although I can configure 3 firewalls, one for each, there are points in the third category of routes (for example GET /object/1

) where it should be accessible anonymously, but if the user provides a valid JWT token , I need to access this user in order to perform some additional logic on the data that I am passing in the response.

Since I currently have a setup (more on my config below), this is all or nothing: I either restrict the entire firewall to only authenticated users with a specific role, or I expose them to anonymous users (and therefore cannot view user information).

Is it possible to have a route that any user can take, but can also see registered users?

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

      

Also, I tried a different approach where I map all routes under the same firewall, but then secure some of them with config securty.access_rules

, but that doesn't 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')
);

      

+3


source to share


3 answers


You can use $ app ['security.jwt.encoder'] to decode the jwt and either create a custom trait and extend the route object with either route level midddleware or an easier way would be to use application level middleware. I had a similar problem 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 in any way by doing this

 $app->get('/object/1', function(Request $request) {

     $decodedJWT = $request->decodedJWT;

     // do whatever logic you need here 
  })

      

+7


source


So: so far I have not found that this is possible thanks to the "normal" way, which is frustrating. I will not mark what I have detailed below as "answering" for a few days, hoping that someone can call back and suggest a better, more "official" way to resolve the dilemma.

TL; The DR: . I manually check the request headers for the access token string and then decode the token using JWT classes to load the user account on routes outside of the firewall. It is incredibly hacked, it feels completely messy, but this is the only solution to the problem I see at the moment.

Technical details . You must first get the token value from the request header. Your controller method will be passed an object Symfony\Component\HttpFoundation\Request

from which you can access $request->headers->get('X-Access-Token')

. In most cases, the user will not be authenticated, so it will be empty and you can return null.

If it is not empty, you should use a Silex instance JWTEncoder

to decode the contents of the token, create a new instance of the token JWTToken

, set the context to the decoded value from the encoder, and finally you can access the username from the specified token, which can then be used to capture the corresponding custom records. An example of what I ran into:

$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 needs to know that since the request is outside the firewall, there is a zero guarantee that the user will be returned (hence a cracked try-catch that throws exceptions off by simply returning null

).



Edit . I have updated the code here to use the built-in DI Silex container (provided by Pimple) so there is no need to create a new JWT encoder instance manually. I also mark @ user5117342's answer as correct, as using some sort of Silex middleware approach is much more reliable.

Edit (Apr 2016) . Using the updated cnam / security-jwt-service 2.1.0 and Symfony / security 2.8 there is a small update that makes the above code 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 problem with newer dependencies is that the constructor JWTToken

requires 3 parameters, which is difficult to obtain in most service levels, not to mention that it is completely irrelevant. When I updated my Composer dependencies, I figured out that I really didn't need to create JWTToken

to get the username I wanted .

Of course, it should be noted that I only use this method for the public (anonymous) API routes to provide some subtleties to the users who are logged in - my application is not dealing with sensitive data, so I am not too concerned with this passage outside of firewalls. In the worst case, the black hat user will end up seeing non-sensitive data, which they usually wouldn't, but what it is. So YMMV.

+1


source


You have to use regex for example.

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

      

0


source







All Articles