Symfony: Convert InsufficientAuthenticationException to HttpAccessDeniedException

Created on 10 Jul 2013  路  54Comments  路  Source: symfony/symfony

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt|fragment)|css|images|js)/
            security: false

        main:
            pattern:    /
            ololo: ~
            provider: users_entity
            anonymous: ~

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

open as anonymous something like /admin/dashboard and got 500. Expected 403.

[2013-07-10 17:27:49] security.INFO: Populated SecurityContext with an anonymous Token [] []
[2013-07-10 17:27:49] security.INFO: No expression found; abstaining from voting. [] []
[2013-07-10 17:27:49] security.DEBUG: Access is denied (user is not fully authenticated) by "/vhosts/www.ololo.com/new/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php" at line 73; redirecting to authentication entry point [] []
[2013-07-10 17:27:49] request.CRITICAL: Uncaught PHP Exception Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException: "Full authentication is required to access this resource." at /vhosts/www.ololo.com/new/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php line 109 {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\InsufficientAuthenticationException: Full authentication is required to access this resource. at /vhosts/www.ololo.com/new/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php:109, Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException: Access Denied at /vhosts/www.ololo.com/new/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:73)"} []
Bug Security Needs Review

Most helpful comment

Any update on this?

All 54 comments

This is weird. It should be handled by the ExceptionController of the security component already

Please explain a little bit particularly why.

https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php is the exception listener I'm talking about. The Firewall is registering on the kernel.exception event to handle these exceptions.

Oh, I see. It causes because my configuration hasn't login_form. But security component hasn't ExceptionController.

@Koc can you confirm there is no framework problem here and close this issue?

I cann't confitm that.

@Koc then can you point the real issue and write a failing test?

I think that better ask @schmittjoh is this fw problem or no.

Might be related: #9823

@Koc #9879 should have fixed this issue. Can you confirm?

No, this issue isn't fixed. I got

Full authentication is required to access this resource.
500 Internal Server Error - InsufficientAuthenticationException
1 linked Exception:  AccessDeniedException

Tryed on the 0285bfde5d9dfe0522c59dab8b28a297f029bedb revision

I can confirm this with Symfony 2.4.2 and using simple_preauth.

Also confirming for 2.4.2, I think I know how this occurs in my setup: I have an Api which uses a token supplied via a header to authenticate. There is no authentication entry point: if there's no token supplied, a 401 response should be returned directly.

This line shows that the exception is re-thrown when this is the case.

Other problem: I'm logged via rememberme. Throwing AccessDeniedException from controller and got redirected to the login. Of course login redirects me to the frontpage because I'm already authenticated. And all this madness goes instead of displaying simle page with http 403 code.

@Koc this is an issue in your login page. If you are authenticated through remember_me only, you should still be able to access the login form to login again as a fully authenticated user

@pkruithof did you find workaround for this?

@nostrzak yes I did: I added an exception listener that checks for these exceptions and sets the right response: https://github.com/financial-media/FMKeystoneBundle/blob/master/src/FM/KeystoneBundle/EventListener/ExceptionListener.php

The listener must have a high priority, else it's picked up by the firewall listener (IIRC).

I don't know if I was blind before but in the cookbok entry there is section exactly about this. Just for reference.

Ping: @pkruithof

Seems it was added in 2.4, I never noticed it, thanks for the link!

Can we close this one?

@fabpot no, please see my comment https://github.com/symfony/symfony/issues/8467#issuecomment-31322874 above

@fabpot Can confirm this is an issue when using simple_preauth and the anonymous firewall flag.

To get this issue moving again, I set up a symfony-standard branch with a failing test:

https://github.com/pkruithof/symfony-standard/tree/issue-8467-invalid-status-codes

Basically, I've set up two firewalls with two authentication mechanisms. The first one is stateless username/password authentication via a POST request with a JSON body, like this:

curl -XPOST -d '{"username":"foo","password":"bar"}' http://localhost/api/tokens

This returns a token which can be used in the second authentication mechanism, using it as a query parameter. The second mechanism is set up exactly as described in this cookbook article.

The difference between the two is that the second uses the simple_preauth key in the firewall, while the first one has a custom authentication provider. I've written two tests which do the same for either firewalls: authenticate with valid and invalid credentials. The simple_preauth firewall returns proper 401/403 responses, but the other one returns 500 responses:

$> ./bin/phpunit -c app --filter ApiKeyTest                                                                                               PHPUnit 4.1.4 by Sebastian Bergmann.

Configuration read from /Users/peter/projects/symfony-standard/app/phpunit.xml.dist

....

Time: 646 ms, Memory: 20.00Mb

OK (4 tests, 4 assertions)
$> ./bin/phpunit -c app --filter TokenTest                                                                                                PHPUnit 4.1.4 by Sebastian Bergmann.

Configuration read from /Users/peter/projects/symfony-standard/app/phpunit.xml.dist

.FF

Time: 795 ms, Memory: 20.25Mb

There were 2 failures:

1) Acme\ApiBundle\Tests\Functional\TokenTest::testNoAuthentication
Failed asserting that 500 matches expected 401.

/Users/peter/projects/symfony-standard/src/Acme/ApiBundle/Tests/Functional/TokenTest.php:35

2) Acme\ApiBundle\Tests\Functional\TokenTest::testBadCredentials
Failed asserting that 500 matches expected 401.

/Users/peter/projects/symfony-standard/src/Acme/ApiBundle/Tests/Functional/TokenTest.php:50

FAILURES!                            
Tests: 3, Assertions: 4, Failures: 2.

So hopefully with this failing test we can actually fix this issue. As I stated earlier, I think this is because the failing firewall does not have an entry point, which is a design choice (there is no login form or anything in this example). And I guess the simple_preauth firewall has this covered. If this is the case, something like lexik/LexikJWTAuthenticationBundle#7 will fix it, but I'm not sure if that's the way Symfony should handle it.

Confirming. Same behaviour for me. InsufficientAuthenticationException is returned with 500 code.
Having onAuthenticationFailure() handler doesn't help either because it never arrives there.

It seems $exception->code and $exception->statusCode got mixed up somewhere along the way but that's ok since FlattenException ignores them both altogether :) Anyway I was able to workaround the issue with a custom error controller.

Also an issue here using the Symfony 2.5.6 and the simple_preauth. Though I'd wholly entertain the notion that it's simply a configuration issue on my end, as others above seem to have some semblance of success.

We are using simple_preauth and we have the same problem.

I have the same problem when trying to configure authentication (http_basic or form_login) in an Apache CGI environment. Locally, it works fine using the integrated PHP web server. Tried with Symfony 2.5 and 2.6 with no difference.

Same problem here with Symfony 2.6.3 and a custom authentification provider (implementing OAuth bearer token http header) i created following the WSSE sf2 doc. When i call my webservice somewhere the ROLE_USER is required, the framework return a 500 (InsufficientAuthenticationException).

Anything new on this issue? Same problem with latest stable version. Even mentioned cookbok entry does not work for me.

Anyone willing to work on this issue, my test case is still available.

@pkruithof actually your test does not seem valid to me ;) It fails due to some other exceptions.

With those changes your tests pass:

https://github.com/pkruithof/symfony-standard/compare/issue-8467-invalid-status-codes...dmaicher:issue-8467-invalid-status-codes?expand=1

As described here:

http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html#the-listener

Hi there!

I still have this issue with symfony 2.7.3. We need to restrict the access of our internal api to some specific IP. This is done in our security.yml

    - { path: ^/api/int/, roles: IS_AUTHENTICATED_ANONYMOUSLY, ips:  ['x.x.x.x'] }
    - { path: ^/api/int/, roles: ROLE_NO_ACCESS }

When we first try to connect to the API with and invalid IP, we get a 500 status code.
Then we add the IP in the allowed list and connect with our login entry point, all fine.
We remove the IP from the list, then we receive the 403 error code. The server remember that we are authentified so no more Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException are triggered, only
Symfony\Component\Security\Core\ExceptionAccessDeniedException.

My first impression is that InsufficientAuthenticationException should not be thrown if a HTTP Exception is running...
I think it is not good to erase an Exception by another in an ExceptionListener, in this case, it erase the first -expected- status code, which is a strict requirement in my project.

Plus it is very hard to manipulate Symfony/Component/Security/Http/Firewall/ExceptionListener since the listener is dynamically added by Symfony/Component/Security/Http/Firewall.

In order to fix my problem, I need to unregister "Symfony/Component/Security/Http/Firewall/ExceptionListener" to benefit of a custom subclass having the correct behavior. I didn't found out how I will implements that. Might be something to improve/invoid in the future :s

Still wondering this has not been fixed for this long?
Seems to me there needs to be a check prior the line 131

$event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException));

to check whether there is a login_path parameter, then check whether there is access_denied_url on a given firewall

Because in my case I'm writing API that has no form login whatsoever and uses stateless firewall, thus having no form_login has no where to jump, but to the default error403.json.twig. When Anonymous is disabled on said firewall I get completely different issue with A Token was not found in the TokenStorage. on `vendor\symfony\symfony\src\Symfony\Component\Security\Http\Firewall\AccessListener.phpwhich is understandable, but I hopedExceptionListenerwould have been called priorAccessListener``, but that's a whole other issue.

the only possible workaround to this is to implement entry_point of a firewall.

<?php
namespace Acme\ApiBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

class EntryPoint implements AuthenticationEntryPointInterface{
  public function start(Request $request, AuthenticationException $authException = null){
      $response = new Response(
        '{"error":{"code":'.Response::HTTP_FORBIDDEN.',"message":"Access Denied"}}',
        Response::HTTP_FORBIDDEN,
        array('Content-Type'=>'application/json'));
      return $response;
  }
}

I have this error as well, it should be AccessDenied exception not 500. Security is defined this way:

firewalls:
    # Internal REST API
    internal:
        pattern: ^/api
        anonymous: true

access_control:
    # REST APIs have their own authentication
    - { path: ^/api/.*, roles: IS_AUTHENTICATED_ANONYMOUSLY, host: 'sn.connect.intern' }
    - { path: ^/api/.*, roles: IS_AUTHENTICATED_FULLY }

@pkruithof thank you for providing code that would reproduce this issue. Your case could be easily solved by doing what was previously suggested by @dmaicher and the docs - catching the AuthenticationException and setting a 401/403 response on the event (one of the exceptions was actually a PHPUnit_Framework_Error).

I'm assuming @Koc's problem has the same root cause as @pkruithof (please provide some code if this is not the case).

To summarise options to solve this issue (already mentioned in the comments above), we could:

From the end-user's perspective option 2 would be the best, as he wouldn't have to do anything to get a correct response. It is a behaviour change though, and we need to consider if this is a bug fix or could it be a BC break (could anyone rely on this behaviour)?

If option 2 is not viable, then I'd vote for option 1.

@jakzal we've since implemented the third option (entry point). However I do still think this is a workaround, rather than a solution. As I mentioned before:

I think this is because the failing firewall does not have an entry point, which is a design choice (there is no login form or anything in this example).

The entry point interface clearly states that it's:

used to start the authentication scheme

So when returning a 401 other than http basic auth (which provides a login form in the browser), the response does not start any form of authentication, but rather denies access.

If option 2 is not viable, then I'd vote for option 1.

I agree.

Also having the same issue, I'm giving ROLES depending on the current application theme (using the LiipThemeBundle) to anonymous users. I don't have login form at all. I have several actions that should be only be accessed for a give theme.
Using:

 * @Security("has_role('ROLE_THEME1')")

When browsing THEME2 returns the same error 500:

Full authentication is required to access this resource.
500 Internal Server Error - InsufficientAuthenticationException
1 linked Exception: AccessDeniedException 禄

I am encountering this issue with the FrameworkExtraBundle's @Security annotation. When the expression fails, the SecurityListener throws an AccessDeniedException as expected, but it is not handled because I do not have form_login or an entry point defined.

Because the exceptions are not occurring in the authentication listeners for me, I am not able to do points 1 or 2 proposed by @jakzal. The suggested EntryPoint implementation added to my firewall fixed the problem.

security:
        api:
            anonymous: true
            entry_point: "system.api_entry"
<?php

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

class ApiEntryPoint implements AuthenticationEntryPointInterface
{
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new JsonResponse([
            'error' => JsonResponse::HTTP_FORBIDDEN,
            'message' => $authException ? $authException->getMessage() : 'Access Denied'
        ], JsonResponse::HTTP_FORBIDDEN);
    }
}

Any update on this?

~@maarekj solution works well :)~
@merk solution works well :)

@goetas Sorry, but which solution ?

@maarekj sorry typo... i meant @merk

I'm still seeing this issue in 3.3 using LexikJWTAuthenticationBundle

@weaverryan @chalasr @stof as you are mergers of the security component: What do you think?

@jakzal proposed some options here: https://github.com/symfony/symfony/issues/8467#issuecomment-163670549

One more idea would be to throw an AccessDeniedHttpException here instead of re-throwing the AuthenticationException:

https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php#L183

I could work on a patch if we agree on a solution :blush:

I had the same issue on 3.4.0-BETA3
I resolved it putting the backend before the public firewall:

Pattern always must be restrictive at the start, and be degressive

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        backend:
            anonymous: ~
            pattern:    ^/admin
            provider: admin

            form_login:
                login_path: admin_login
                check_path: admin_login
                default_target_path: admin_dashboard

        public:
            anonymous: ~
            pattern:    ^/
            provider: customer

    access_control:
        - { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: ROLE_ADMIN }

I get the same kind of error with Symfony4 + Flex (this is a proof of concept project)
A route protected by a firewall and grant access return a 500 AccessDeniedException instead of a 403.

A sample route that return the 500 :
http://localhost:85/demo/login/json/isloggedin

Here is my security.yaml

security:
    encoders:
        # @todo should use password encoding, more info here: https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password
        Symfony\Component\Security\Core\User\User: plaintext
    # https://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        in_memory:
            memory:
                users:
                    test:
                        password:           test
                        roles:              ROLE_USER
                    admin:
                        password:           admin
                        roles:              [ROLE_USER, ROLE_ADMIN]

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        vuejs:
            pattern: ^/(demo/(vuejs|form|login/(json|json/isloggedin|json/logout))|api)
            anonymous: ~
            json_login:
                check_path: /demo/login/json
                # this doesn't work, see in the security.yaml:12 for more explanation
                #check_path: demo_login_json_check
            logout:
                path:   demo_login_json_logout
                target: index
                invalidate_session: true
        main:
            pattern: ^/demo/login/(secured|standard|authenticate|logout)
            anonymous: ~

            # activate different ways to authenticate

            # http_basic: ~
            # https://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # https://symfony.com/doc/current/cookbook/security/form_login_setup.html

            form_login:
                default_target_path: demo_secured_page
                login_path: demo_login_standard
                check_path: demo_login_standard_check
                # field names for the username and password fields
                username_parameter: login_username
                password_parameter: login_password

                # csrf token options
                csrf_parameter:       "%csrf_token_parameter%"
                csrf_token_generator: security.csrf.token_manager
                csrf_token_id:        "%csrf_token_id%"

            logout:
                path:   demo_login_standard_logout
                target: index
                invalidate_session: true

Here is the action controller:

    /**
     * Try to test this security when the one on the bottom works Security("is_granted('IS_AUTHENTICATED_FULLY')")
     *
     * call it with .json extension and check if you have a 200
     *
     * @todo: should we let it as is, or always return a 200 and in the Json content set the isLoggedIn to 0 or 1 ?
     * For instance i stay on my first choice
     *
     * @Security("is_granted('IS_AUTHENTICATED_FULLY')")
     * @Route(
     *     "/demo/login/json/isloggedin",
     *     name="demo_secured_page_is_logged_in",
     *     )
     * @Method({"GET"})
     */
    public function isLoggedIn() {
        // will be usefull if we decide to return always 200 + the real Json content represented by isLoggedIn: 0|1
        $authenticated = $this->isGranted('IS_AUTHENTICATED_FULLY');

        return new JsonResponse(['isLoggedIn' => (int)$authenticated, ]);
    }

And finally the composer.json:

{
    "type": "project",
    "license": "proprietary",
    "require": {
        "php": "^7.0",
        "ext-curl": "*",
        "ext-pdo_sqlite": "*",
        "doctrine/doctrine-migrations-bundle": "^1.3",
        "javiereguiluz/easyadmin-bundle": "^1.17",
        "php-http/httplug-pack": "^1.0",
        "rebolon/api-pack": "^1.0.2",
        "sensio/framework-extra-bundle": "^5.1.0",
        "symfony/console": "^4.0.0",
        "symfony/framework-bundle": "^4.0.0",
        "symfony/monolog-bundle": "^3.1.0",
        "symfony/profiler-pack": "^1.0",
        "symfony/yaml": "^4.0.0",
        "webonyx/graphql-php": "^0.11.5"
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0",
        "symfony/dotenv": "^4.0.0",
        "symfony/flex": "^1.0",
        "symfony/webpack-encore-pack": "^1.0"
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "repositories": [
        {
            "type": "vcs",
            "url":  "[email protected]:Rebolon/api-pack.git"
        }
    ],
    "config": {
        "preferred-install": {
            "*": "dist"
        },
        "sort-packages": true
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "auto-scripts": {
            "cache:clear": "symfony-cmd",
            "assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd"
        },
        "post-install-cmd": [
            "@auto-scripts"
        ],
        "post-update-cmd": [
            "@auto-scripts"
        ]
    },
    "conflict": {
        "symfony/symfony": "*",
        "symfony/twig-bundle": "<3.3",
        "symfony/debug": "<3.3"
    },
    "extra": {
        "symfony": {
            "id": "01BXPDHBAP11MNR97CR7E97D3F",
            "allow-contrib": false
        }
    }
}

And maybe the way i installed the app:

composer create-project symfony/skeleton sf-flex-encore-vuejs
cd sf-flex-encore-vuejs
composer req encore annotations twig api http profiler log doctrine-migrations admin webonyx/graphql-php
composer require --dev doctrine/doctrine-fixtures-bundle

Here is the project uri: https://github.com/Rebolon/php-sf-flex-webpack-encore-vuejs

when i look at the Security component documentation https://symfony.com/doc/master/bundles/SensioFrameworkExtraBundle/annotations/security.html

I can see that we have to specify the status_code if we want an HTTP Exception instead of an AccessDeniedException

For my configuration it seems that the json_login is the explanation of the problem because this is its own behavior (why ? i don't know the code always throw this exception, not an HttpException)

If this would help.

I'm encountering this issue with the following configuration:

providers:
    myProvider:
        id: myProviderService

firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false

    main:
        anonymous: ~

    application_main:
        pattern: ^/
        provider: myProvider:
        anonymous: ~
        myAuthenticator:
    login_path: login_route
        logout:
            path: /logout
            target: index
            invalidate_session: false
            handlers: [myLogoutHandler]

The problem is the presence main firewall and may be the way it is configured. When I remove it and leave only dev and application_main everything works as normal.

@chaos-drone your main firewall catches all requests that do not match the pattern of the dev firewall. Thus the application_main firewall is never reached.

I hope #28801 fixes this issue

@Koc your fix is only for 2.8 or also for 3.4+ and 4+ ?

@Rebolon it will be merged up in the other branches :wink:

Was this page helpful?
0 / 5 - 0 ratings