title: Symfony5 / write a Custom Authenticator with API always return me Invalid credentials
body: <p>it's simple.</p>
  <p>I need to validate a user via external and sticky the session, but at the moment, <strong>I want to validate any user</strong>.</p>
  <p>I'm following the Custom Authenticator doc: <a href="https://symfony.com/doc/current/security/custom_authenticator.html" rel="nofollow noreferrer">https://symfony.com/doc/current/security/custom_authenticator.html</a></p>
  <p>I create a User and UserProvider without Doctrine:</p>
  <pre><code>\App\Security\User
  \App\Security\UserProvider
  </code></pre>
  <p>And I create the custom authentificator: <code>\App\Security\ApiAuthentificator.php</code> and always return a valid SelfValidatingPassport:</p>
  <pre class="lang-php prettyprint-override"><code>&lt;?php
  // src/Security/ApiAuthenticator.php
  namespace App\Security;
  
  use Symfony\Component\HttpFoundation\JsonResponse;
  use Symfony\Component\HttpFoundation\Request;
  use Symfony\Component\HttpFoundation\Response;
  use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  use Symfony\Component\Security\Core\Exception\AuthenticationException;
  use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
  use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
  use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
  
  use Symfony\Component\HttpFoundation\RedirectResponse; 
  
  
  use App\Security\UserProvider;
  
  class ApiAuthenticator extends AbstractAuthenticator
  {
      private $userProvider;
  
      public function __construct(UserProvider $userProvider)
      {
          $this-&gt;userProvider = $userProvider;
      }
  
      /**
       * Called on every request to decide if this authenticator should be
       * used for the request. Returning `false` will cause this authenticator
       * to be skipped.
       */
      public function supports(Request $request): ?bool
      {
          return 'login' === $request-&gt;attributes-&gt;get('_route')
              &amp;&amp; $request-&gt;isMethod('POST');
      }
  
      public function authenticate(Request $request): PassportInterface
      {
          /*
          $apiToken = $request-&gt;headers-&gt;get('X-AUTH-TOKEN');
          if (null === $apiToken) {
              // The token header was empty, authentication fails with HTTP Status
              // Code 401 &quot;Unauthorized&quot;
              throw new CustomUserMessageAuthenticationException('No API token provided');
          }
          
          return new SelfValidatingPassport(new UserBadge($apiToken));
          */
  
          //
          // TODO: call to API
          // 
          
          $credentials = [
              'username' =&gt; $request-&gt;request-&gt;get('_username'),
              'password' =&gt; $request-&gt;request-&gt;get('_password')
          ];
          
          return new SelfValidatingPassport(new UserBadge($credentials['username']));
      }
  
      public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
      {
          // on success, let the request continue
          return null;
          // return new RedirectResponse($this-&gt;urlGenerator-&gt;generate('admin'));
      }
  
      public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
      {
          $data = [
              // you may want to customize or obfuscate the message first
              'message' =&gt; strtr($exception-&gt;getMessageKey(), $exception-&gt;getMessageData())
  
              // or to translate this message
              // $this-&gt;translator-&gt;trans($exception-&gt;getMessageKey(), $exception-&gt;getMessageData())
          ];
  
          // return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
          return new Response('&lt;html&gt;&lt;body&gt;' . $data['message'] . '&lt;/body&gt;&lt;/html&gt;');
      }
  }
  
  </code></pre>
  <p>But, when I do login, the Login process response Invalid Credentials, but it's not the onAuthenticationFailure function on ApiAuthenticator.</p>
  <p><a href="https://i.stack.imgur.com/Wk4vU.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/Wk4vU.png" alt="enter image description here" /></a></p>
  <p>In the UserProvider, always return a User in the loadUserByIdentifier, and the refreshUser it's never called because the firewall is <code>stateless: true</code>:</p>
  <pre class="lang-php prettyprint-override"><code>&lt;?php
  // src/Security/UserProvider
  namespace App\Security;
  
  use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
  use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
  use Symfony\Component\Security\Core\User\UserInterface;
  use Symfony\Component\Security\Core\User\UserProviderInterface;
  
  use Symfony\Component\HttpFoundation\RedirectResponse;
  use Symfony\Component\HttpFoundation\Request;
  
  use App\Security\User;
  use App\Utils\HelpCurl;
  
  
  class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
  {
      /**
       * Symfony calls this method if you use features like switch_user
       * or remember_me.
       *
       * If you're not using these features, you do not need to implement
       * this method.
       *
       * @throws UserNotFoundException if the user is not found
       */
      public function loadUserByIdentifier($identifier): UserInterface
      {
          // Load a User object from your data source or throw UserNotFoundException.
          // The $identifier argument may not actually be a username:
          // it is whatever value is being returned by the getUserIdentifier()
          // method in your User class.
  
          $user = new User();
          $user-&gt;setUsername($identifier);
  
          return $user;
  
          /*
          throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__);
          */
      }
  
      /**
       * @deprecated since Symfony 5.3, loadUserByIdentifier() is used instead
       */
      public function loadUserByUsername($username): UserInterface
      {
          return $this-&gt;loadUserByIdentifier($username);
      }
  
      /**
       * Refreshes the user after being reloaded from the session.
       *
       * When a user is logged in, at the beginning of each request, the
       * User object is loaded from the session and then this method is
       * called. Your job is to make sure the user's data is still fresh by,
       * for example, re-querying for fresh User data.
       *
       * If your firewall is &quot;stateless: true&quot; (for a pure API), this
       * method is not called.
       *
       * @return UserInterface
       */
      public function refreshUser(UserInterface $user)
      {
          if (!$user instanceof User) {
              throw new UnsupportedUserException(sprintf('Invalid user class &quot;%s&quot;.', get_class($user)));
          }
  
          throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
  
          /*
          // Return a User object after making sure its data is &quot;fresh&quot;.
          // Or throw a UsernameNotFoundException if the user no longer exists.
          throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
          */
      }
  
      /**
       * Tells Symfony to use this provider for this User class.
       */
      public function supportsClass($class)
      {
          return User::class === $class || is_subclass_of($class, User::class);
      }
  
      /**
       * Upgrades the hashed password of a user, typically for using a better hash algorithm.
       */
      public function upgradePassword(UserInterface $user, string $newHashedPassword): void
      {
          // TODO: when hashed passwords are in use, this method should:
          // 1. persist the new password in the user storage
          // 2. update the $user object with $user-&gt;setPassword($newHashedPassword);
      }
  }
  
  </code></pre>
  <p><strong>Any ideas? What is missing?</strong></p>
  <p>I paste the security and the User:</p>
  <pre class="lang-php prettyprint-override"><code>&lt;?php
  // src/Security/User
  namespace App\Security;
  
  use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
  use Symfony\Component\Security\Core\User\UserInterface;
  
  class User implements UserInterface
  {
      private $username;
  
      private $roles = [];
  
      /**
       * @deprecated since Symfony 5.3, use getUserIdentifier instead
       */
      public function getUsername(): string
      {
          return (string) $this-&gt;username;
      }
  
      public function setUsername(string $username): self
      {
          $this-&gt;username = $username;
  
          return $this;
      }
  
      /**
       * A visual identifier that represents this user.
       *
       * @see UserInterface
       */
      public function getUserIdentifier(): string
      {
          return (string) $this-&gt;username;
      }
  
      /**
       * @see UserInterface
       */
      public function getRoles(): array
      {
          $roles = $this-&gt;roles;
          // guarantee every user at least has ROLE_USER
          $roles[] = 'ROLE_CUSTOMER';
  
          return array_unique($roles);
      }
  
      public function setRoles(array $roles): self
      {
          $this-&gt;roles = $roles;
  
          return $this;
      }
  
      /**
       * This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
       *
       * @see PasswordAuthenticatedUserInterface
       */
      public function getPassword(): ?string
      {
          return null;
      }
  
      /**
       * This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
       *
       * @see UserInterface
       */
      public function getSalt(): ?string
      {
          return null;
      }
  
      /**
       * @see UserInterface
       */
      public function eraseCredentials()
      {
          // If you store any temporary, sensitive data on the user, clear it here
          // $this-&gt;plainPassword = null;
      }
  }
  
  </code></pre>
  <pre class="lang-yaml prettyprint-override"><code>security:
      # https://symfony.com/doc/current/security/authenticator_manager.html
      enable_authenticator_manager: true
      # https://symfony.com/doc/current/security.html#c-hashing-passwords
      password_hashers:
          Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
      # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
      providers:
          # used to reload user from session &amp; other features (e.g. switch_user)
          app_user_provider:
              id: App\Security\UserProvider
      firewalls:
          dev:
              pattern: ^/(_(profiler|wdt)|css|images|js)/
              security: false
          main:
              # lazy: true
              stateless: true
              # provider: users_in_memory
  
              # activate different ways to authenticate
              # https://symfony.com/doc/current/security.html#firewalls-authentication
  
              # https://symfony.com/doc/current/security/impersonating_user.html
              # switch_user: true
              custom_authenticators:
                  - App\Security\ApiAuthenticator
              form_login:
                  login_path: login
                  check_path: login
              logout:
                  path: app_logout
  
      # Easy way to control access for large sections of your site
      # Note: Only the *first* access control that matches will be used
      access_control:
          # - { path: ^/admin, roles: ROLE_ADMIN }
          # - { path: ^/profile, roles: ROLE_USER }
  
  </code></pre>