src/Security/Authenticator/TokenAuthenticator.php line 70

Open in your IDE?
  1. <?php
  2. //----------------------------------------------------------------------
  3. // src/Security/Authenticator/TokenAuthenticator.php
  4. //----------------------------------------------------------------------
  5. namespace App\Security\Authenticator;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  9. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  10. use Symfony\Component\Security\Core\Exception\BadCredentialsException;
  11. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  12. use Symfony\Component\Security\Core\User\UserInterface;
  13. use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
  14. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  15. use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
  16. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  17. use App\Entity\APIRest\AccessAPI;
  18. use App\Services\APIRest\Tools\APIResponseTools;
  19. use App\Services\APIRest\Tools\RouteScopeResolver;
  20. use App\Utils\APIError;
  21. class TokenAuthenticator extends AbstractAuthenticator
  22. {
  23.     public function __construct(APIResponseTools $apiResponseToolsRouteScopeResolver $routeScopeResolver)
  24.     {
  25.         $this->apiResponseTools $apiResponseTools;
  26.         $this->routeScopeResolver $routeScopeResolver;
  27.     }
  28.     /**
  29.      * Called on every request to decide if this authenticator should be
  30.      * used for the request. Returning `false` will cause this authenticator
  31.      * to be skipped.
  32.      */
  33.     public function supports(Request $request): ?bool
  34.     {
  35.         $route $request->attributes->get('_route');
  36.         // Check if it's an API call
  37.         if (preg_match("/api_rest_/"$route) !== 1)
  38.         {
  39.             return false;
  40.         }
  41.         return true;
  42.     }
  43.     /**
  44.      * Try to authenticate user with request param
  45.      */
  46.     public function authenticate(Request $request): Passport
  47.     {
  48.         // Check headers contain 'X-Auth-Token' or 'Authorization' param
  49.         $this->checkHeaders($request);
  50.         // Get token sent
  51.         $token $this->getToken($request);
  52.         if ($token === null)
  53.         {
  54.             // The token header was empty, authentication fails with HTTP Status
  55.             // Code 401 "Unauthorized"
  56.             $errorMessage APIError::AUTHENTICATION_INVALID_TOKEN['errorMessage'];
  57.             throw new CustomUserMessageAuthenticationException($errorMessage);
  58.         }
  59.         // Check User is valid (AcessAPI, token valid, User Active)
  60.         $customCredential = new CustomCredentials(
  61.             function (string $credentialsUserInterface $user): bool
  62.             {
  63.                 if (!($user instanceof AccessAPI))
  64.                 {
  65.                     return false;
  66.                 }
  67.                 if ($credentials !== $user->getAccessToken())
  68.                 {
  69.                     return false;
  70.                 }
  71.                 if ($user->isAccessTokenExpired())
  72.                 {
  73.                     return false;
  74.                 }
  75.                 if (!$user->getIsActive())
  76.                 {
  77.                     return false;
  78.                 }
  79.                 return true;
  80.             },
  81.             // The custom credentials
  82.             $token
  83.         );
  84.         return new Passport(new UserBadge($token), $customCredential);
  85.     }
  86.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $firewallName): ?Response
  87.     {
  88.         // Scope control is currently in observation mode:
  89.         // - routes are not yet annotated with RequiredScopes
  90.         // - missing scopes must not block access
  91.         $accessAPI $token->getUser();
  92.         if (!($accessAPI instanceof AccessAPI))
  93.         {
  94.             return null;
  95.         }
  96.         // Get required scopes for the current route
  97.         // The required scopes are defined in the controller method using the RequiredScopes attribute
  98.         $requiredScopes $this->routeScopeResolver->getRequiredScopes($request);
  99.         // No route currently uses RequiredScopes; even if it did, we do not block yet.
  100.         // This hook is here to prepare future enforcement once scopes are widely used.
  101.         if (empty($requiredScopes))
  102.         {
  103.             return null;
  104.         }
  105.         // In future we could enforce that at least one required scope is present:
  106.         $hasRequiredScope false;
  107.         foreach ($requiredScopes as $requiredScope)
  108.         {
  109.             if ($accessAPI->hasScope($requiredScope))
  110.             {
  111.                 $hasRequiredScope true;
  112.                 break;
  113.             }
  114.         }
  115.         
  116.         if (!$hasRequiredScope)
  117.         {
  118.             $apiError = new APIError(APIError::AUTHENTICATION_SCOPE_FORBIDDEN);
  119.             return $this->apiResponseTools->unauthorizedResponse($apiError);
  120.         }
  121.         return null;
  122.     }
  123.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): ?Response
  124.     {
  125.         // Prevent warning for size overload
  126.         ob_get_contents();
  127.         ob_end_clean();
  128.         // Avoid symfony error caused by CustomCredentials return false
  129.         if ($exception instanceof BadCredentialsException)
  130.         {
  131.             $apiError = new APIError(APIError::AUTHENTICATION_FAILED);
  132.         }
  133.         else
  134.         {
  135.             // Classic case
  136.             $apiError = new APIError(APIError::AUTHENTICATION_FAILED$exception->getMessage());
  137.         }
  138.         
  139.         return $this->apiResponseTools->unauthorizedResponse($apiError);
  140.     }
  141.     // ----- Custom methods ----- //
  142.     /**
  143.      * Check headers has 'X-Auth-Token' or 'Authorization' param (Else throw Exception to trigger 'onAuthenticationFailure' method)
  144.      * @throws CustomUserMessageAuthenticationException
  145.      */
  146.     protected function checkHeaders(Request $request)
  147.     {
  148.         if (!($request->headers->has('X-Auth-Token') || $request->headers->has('Authorization')))
  149.         {
  150.             $errorMessage APIError::AUTHENTICATION_REQUIRED['errorMessage'];
  151.             throw new CustomUserMessageAuthenticationException($errorMessage);
  152.         }
  153.     }
  154.     protected function getToken(Request $request): ?string
  155.     {
  156.         if ($request->headers->has('X-Auth-Token'))
  157.         {
  158.             return $request->headers->get('X-Auth-Token');
  159.         }
  160.         if ($request->headers->has('Authorization') && stristr($request->headers->get('Authorization'), 'Bearer ') != false)
  161.         {
  162.             return str_ireplace('Bearer '''$request->headers->get('Authorization'));
  163.         }
  164.         // Should never happen
  165.         return null;
  166.     }
  167. }