symfony / symfony-docs

The Symfony documentation
https://symfony.com/doc
Other
2.16k stars 5.11k forks source link

Only Authenticating for certain urls in api key authentication #3815

Closed javaguirre closed 10 years ago

javaguirre commented 10 years ago

I think this part is not valid, It couldn't work.

http://symfony.com/doc/current/cookbook/security/api_key_authentication.html#only-authenticating-for-certain-urls

If you just do this:

    public function createToken(Request $request, $providerKey)
    {
        // set the only URL where we should look for auth information
        // and only return the token if we're at that URL
        $targetUrl = '/login/check';
        if (!$this->httpUtils->checkRequestPath($request, $targetUrl)) {
            return;
        }

        // ...
    }

The Token is null and then authenticate won't work because needs an instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface. The exact error is:

ContextErrorException: Catchable Fatal Error: 
Argument 1 passed to Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager::authenticate()
 must be an instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface, null given,

It happens here:

https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php#L77-L80

I am trying to sort things out. :-)

Thank you in advance!

javaguirre commented 10 years ago

I solved it in security.yml creating an excluding regex for the pattern, using the example above:

security:
  ...

  firewalls:
    secured_area:
      pattern: ^/(?!login\/check)

I like the approach better also because this is a security matter, so not having to add the url inside the ApiKeyAuthentication class seems a good deal.

weaverryan commented 10 years ago

Hi @javaguirre!

Your solution sounds good, but I'm not totally sure it should work - but obviously, you have it working, so I'm trying to understand :). If you authenticate at /login_check, that URL must be under your firewall, because when you set the token, you're setting it for whatever firewall you're in. But, that doesn't really matter :).

Because I think your original report may be valid. The createToken function is called here: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/SimpleFormAuthenticationListener.php#L119

And as you can see, the $token we return is passed directly to the authenticate method, which does require a Token object (null is not valid). So, when we return null in the example, I think this is a mistake. Instead, we need to return some token in all cases. Unfortunately, this causes the authenticateToken method to be called, where I think we need to return an authenticatedToken, or throw an exception (which would trigger them being asked to login).

So basically, I think you're right, but I'm not sure immediately what the proper solution is. Your solution doesn't make logical sense to me, but if you want to explain it further, it could be legitimate :).

Thanks!

javaguirre commented 10 years ago

I'll work on a solution, thank you for your answer. :-)

petert82 commented 10 years ago

I ran up against this today, and kind of hacked my way around it by - in the case where I don't want to authenticate a particular URL - setting an empty 'api key' on the token returned by createToken. Then having authenticateToken throw an exception if the 'api key' is empty. Then having onAuthenticationFailure ignore any exceptions where the 'api key' is empty. This works for my use case, but wouldn't if, say an empty api key was really an error for your application.

It would certainly be nice to have a 'canonical' way of doing this, because as noted the current advice in the docs under 'Only Authenticating for Certain URLs' doesn't work!

weaverryan commented 10 years ago

I believe a proposed fix for this is at symfony/symfony#11414.

cirovargas commented 10 years ago

Using this:

public function createToken(Request $request, $providerKey) { // set the only URL where we should look for auth information // and only return the token if we're at that URL $targetUrl = '/login/check'; if (!$this->httpUtils->checkRequestPath($request, $targetUrl)) { return; }

    // ...
}

For me returns : "A Token was not found in the SecurityContext."

How i can solve this?

put on authenticateToken method:
return new PreAuthenticatedToken( 'anon.', '', $providerKey );

????

weaverryan commented 10 years ago

@cirovargas @peterrehm has a workaround here: https://github.com/symfony/symfony/issues/11490#issuecomment-50265924

codymihai commented 9 years ago

I have wrote this code http://symfony.com/doc/current/cookbook/security/api_key_authentication.html#cookbook-security-api-key-config

And now when I ran something like this: app/login/check?apiKey=mihai I receive:cNotFoundHttpException: No route found for "GET /app/login/check".

My config is: * Security / $app->register( new Silex\Provider\SecurityServiceProvider(), array( 'security.firewalls' => array( /'members-area' => array( 'pattern' => '^/app', 'anonymous' => true, 'form' => array( 'login_path' => '/app/login', 'check_path' => '/app/login/check', 'failure_path' => '/app/login', 'default_target_path' => '/app', //'always_use_default_target_path' => true, 'username_parameter' => 'username', 'password_parameter' => 'password', 'use_referer' => true ), 'logout' => array( 'logout_path' => '/app/logout', 'target' => '/app', 'invalidate_session' => true, ), 'users' => $app['user.provider'] ),*/ 'secured_area' => array( 'pattern' => '^/app', 'stateless' => false, 'anonymous' => true, 'form' => array( 'authenticator' => $app['apiKey.authenticator'] ), 'logout' => array( 'logout_path' => '/app/logout', 'target' => '/app', 'invalidate_session' => true, ), 'users' => $app['apiKey.provider'],

        ),
    ),
    'security.providers' => array(
        'api_key_user_provider'  => array(
            'id' => $app['apiKey.provider'],
        ),
    )
)

);

Could someone help me? thanks

weaverryan commented 9 years ago

Hi there?

Just create a route for /app/login/check. It doesn't need to do anything - the controller won't be called, but it has to exist.

Cheers!

codymihai commented 9 years ago

Hi @weaverryan , And should I do something in that route? After I have created: $app->match('/app/login/check', function() use ($app) {

}); The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?

weaverryan commented 9 years ago

If you submit your form, this should result in a POST request to that URL, and then the security system will intercept it (so it won't even hit your controller). If you just surf to the URL, you'll get an error - but people will only submit the login form to get here (make sure you have method=POST on your login form)

codymihai commented 9 years ago

@weaverryan This part is working with the form login. But what I am trying to do is to login with http://localhost/myenms/web/app/login/check?apikey=mihai and I do not manage to do it. I have done this steps in my project: http://symfony.com/doc/current/cookbook/security/api_key_authentication.html#cookbook-security-api-key-config

And my config is:

$app->register( new Silex\Provider\SecurityServiceProvider(), array( 'security.firewalls' => array( 'members-area' => array( 'pattern' => '^/app', 'anonymous' => true, 'form' => array( 'login_path' => '/app/login', 'check_path' => '/app/login/check', 'failure_path' => '/app/login', 'default_target_path' => '/app', //'always_use_default_target_path' => true, 'username_parameter' => 'username', 'password_parameter' => 'password', 'use_referer' => true ), 'logout' => array( 'logout_path' => '/app/logout', 'target' => '/app', 'invalidate_session' => true, ), 'users' => $app['user.provider'] ), 'secured_area' => array( 'pattern' => '^/app', 'stateless' => false, 'anonymous' => true, 'form' => array( 'authenticator' => $app['apiKey.authenticator'] ), 'logout' => array( 'logout_path' => '/app/logout', 'target' => '/app', 'invalidate_session' => true, ), 'users' => $app['apiKey.provider'],

        ),
    ),
    'security.providers' => array(
        'api_key_user_provider'  => array(
            'id' => $app['apiKey.provider'],
        ),
    )
)

);

codymihai commented 9 years ago

@weaverryan

In the final I have manage to do it. I will post here how I did probably would help someone.

$app->match('/app/login/api/check', function() use ($app) {

$auth = new \Application\Security\ApiKeyAuthenticator($app['security.http_utils']);

$token = $auth->createToken($app['request'], 'mihai');
$result = $auth->authenticateToken($token, $app['apiKey.provider'], "mihai");

$app['security']->setToken($result);
return $app->redirect(
    $app['baseUrl'] . '/app'
);

});