Codeception / module-symfony

Codeception module for testing apps using Symfony framework
MIT License
89 stars 24 forks source link

amLoggedInAs with symfony 7.0 does not work with the acces token handler #186

Open c33s opened 8 months ago

c33s commented 8 months ago

symfony changed their security again. the code to do the automated login looks like that:

https://symfony.com/doc/7.0/security.html#login-programmatically

// get the user to be authenticated
        $user = ...;

        // log the user in on the current firewall
        $security->login($user);

        // if the firewall has more than one authenticator, you must pass it explicitly
        // by using the name of built-in authenticators...
        $security->login($user, 'form_login');
        // ...or the service id of custom authenticators
        $security->login($user, ExampleAuthenticator::class);

        // you can also log in on a different firewall...
        $security->login($user, 'form_login', 'other_firewall');

        // ...and add badges
        $security->login($user, 'form_login', 'other_firewall', [(new RememberMeBadge())->enable()]);

        // use the redirection logic applied to regular login
        $redirectResponse = $security->login($user);
        return $redirectResponse;

having the following config:

security.yaml

security:
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        users_in_memory:
            memory:
                users:
                    api_token:
                        password: <hashed password>.
                        roles: [ROLE_API]
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: users_in_memory
            access_token:
                token_handler: App\Security\AccessTokenHandler
                token_extractors:
                    - 'header'

src/Security/AccessTokenHandler.php

<?php

namespace App\Security;

use Psr\Log\LoggerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;

class AccessTokenHandler implements AccessTokenHandlerInterface
{
    final public const TOKEN_NAME = 'api_token';
    public function __construct(
        private UserProviderInterface $userProvider,
        private UserPasswordHasherInterface $passwordHasher,
        private LoggerInterface $logger,
    ) {
    }

    public function getUserBadgeFrom(?string $accessToken): UserBadge
    {
        if (null === $accessToken || !$this->verifyCredentials($accessToken)) {
            throw new BadCredentialsException('Invalid credentials.');
        }

        return new UserBadge(self::TOKEN_NAME);
    }

    private function verifyCredentials(string $accessToken): bool
    {
        $user = $this->userProvider->loadUserByIdentifier(self::TOKEN_NAME);

        if (get_class($user) !== InMemoryUser::class) {
            $userClass = get_class($user);
            $requiredUserClass = InMemoryUser::class;
            $this->logger->error("Invalid User Class for access token authentication. Got $userClass but require $requiredUserClass");

            return false;
        }

        return $this->passwordHasher->isPasswordValid($user, $accessToken);
    }
}

version.html.twig

...
                        {% if is_granted('ROLE_API') %}
                             {# tried both this and the below code leads to the same #}
                        {% endif %}
                        {% if is_granted('IS_AUTHENTICATED_FULLY') %}    
                            App Version: {{ app_version }}
                        {% endif %}
...

cest

    public function VersionIsVisibleForApiUsersCest(FunctionalTester $I): void
    {
        $user = new InMemoryUser(AccessTokenHandler::TOKEN_NAME, '<my dummy password>', ['ROLES_API']);
        $I->amOnPage('/');
        $I->amLoggedInAs($user, 'main');
        $I->amOnPage('/version');
        $I->see('App Version');
    }

so the old code to autologin doesn't work any more or does not work with the acces token handler. the code from above fails silently, the user is simply not logged in (as far as i can verify).

switching to a custom code which uses the new security helper and its locin helper cest:

        $security = $I->grabService('test.security_helper');
        $security->login($user);

services.yaml:

when@test:
    services:
        test.security_helper:
            alias: security.helper
            public: true

leads to

[Symfony\Component\Security\Core\Exception\LogicException] Unable to login without a request context.

i wanted to try to set the header for this request but only found https://github.com/Codeception/Codeception/issues/5308

any thoughts?

TavoNiievez commented 8 months ago

@c33s Do you think you can replicate this behavior in the test project (codeception/symfony-module-tests) ? This way I could investigate it.