3

I'm working on an API, and I had implemented a JWT to make it stateless. I created an AuthController, which returns a JWT when login information is correct. Here you can see the return code that generates the token:

/* RETURN MESSAGE */
$body = [
    'auth_token' => $jwt,
];
$json = new JsonResponse($body);
$json->setStatusCode(201, "Created");   // Headers
return $json;

This is the result when I run the authenticate method, un the URL localhost:8000/authenticate.
Now, what I would need to do is that, when a user tries to get another / URL, the program doesn't allow him to reach it if he's not passing the Bearer token in the request's header. But it's not working. The platform always allows me to enter any URL without setting an Authorization in the header.
Here's my security file, where I tried to set this:

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'

    encoders:
        App\Entity\ATblUsers:
            algorithm: bcrypt

    providers:
        users_in_memory: { memory: null }

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

        main:
            # Anonymous property is no longer supported by Symfony. It is commented by now, but it will be deleted in
            # future revision:
            # anonymous: true
            guard:
                authenticators:
                    - App\Security\JwtAuthenticator
            lazy: 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
            stateless: true

    # 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 }

And, finally, here's my App\Security\JwtAuthenticator:

namespace App\Security;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Firebase\JWT\JWT;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

class JwtAuthenticator extends AbstractGuardAuthenticator
{
    private $em;
    private $params;

    public function __construct(EntityManagerInterface $em, ContainerBagInterface $params)
    {
        $this->em = $em;
        $this->params = $params;
    }

    public function start(Request $request, AuthenticationException $authException = null): JsonResponse
    {
        $body = [
            'message' => 'Authentication Required',
        ];
        return new JsonResponse($body, Response::HTTP_UNAUTHORIZED);
    }

    public function supports(Request $request): bool
    {
        return $request->headers->has('Authorization');
    }

    public function getCredentials(Request $request)
    {
        return $request->headers->get('Authorization');
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        try{
            $credentials = str_replace('Bearer ', '', $credentials);
            $jwt = (array) JWT::decode($credentials, $this->params->get('jwt_secret'), ['HS256']);
            return $this->em->getRepository('App:ATblUsers')->find($jwt['sub']);
        }catch (\Exception $exception){
            throw new AuthenticationException($exception->getMessage());
        }

    }

    public function checkCredentials($credentials, UserInterface $user)
    {

    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
    {
        return new JsonResponse([
            'message' => $exception->getMessage()
        ], Response::HTTP_UNAUTHORIZED);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
    {
        return;
    }

    public function supportsRememberMe(): bool
    {
        return false;
    }
}

I've been looking at a lot of websites and tutorials, but not anyone is doing exactly what I need or are implementing very basic functionalities that don't match with what I need. Almost all of that websites explain this using Symfony 4, but I'm using Symfony 5, so a lot of functions that use in tutorials are deprecated. Does someone know what I am missing?

FourBars
  • 475
  • 2
  • 14

3 Answers3

3

You are probably missing access_control configuration in security.yaml:

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'

    encoders:
        App\Entity\ATblUsers:
            algorithm: bcrypt

    providers:
        users_in_memory: { memory: null }

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

        main:
            # Anonymous property is no longer supported by Symfony. It is commented by now, but it will be deleted in
            # future revision:
            # anonymous: true
            guard:
                authenticators:
                    - App\Security\JwtAuthenticator
            lazy: 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
            stateless: true

    # 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: ^/authenticate, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }
Slimu
  • 88
  • 8
  • You're probably right, and I had tried it, too. But when adding that `access_control`, I can't even access to `/authenticate`, so I can never login to generate the JWT token. – FourBars Oct 22 '21 at 10:43
  • Oh you're right, you need to add another path with `PUBLIC_ACCESS` so it looks like this: access_control: - { path: ^/authenticate, roles: PUBLIC_ACCESS} - { path: ^/, roles: IS_AUTHENTICATED_FULLY } – Slimu Oct 22 '21 at 10:46
  • For everyone who is in the same situation as me, this answer will solve the problem. But it will be good to check [@Abdelhak ouaddi](https://stackoverflow.com/a/69676082/10643072) answer, too. So you can try the easy bundle instead of firebase. I have it finally working with what Slimu said. It's been necessary some modifications to my original code but finally working. – FourBars Oct 22 '21 at 11:32
3

I have not looked at your code in detail, I just want to tell you that what you are doing hard already exists in a bundle maintained and well documented and you will not need to hardcode, I really invite you to use it is very useful.

https://github.com/lexik/LexikJWTAuthenticationBundle

Dharman
  • 30,962
  • 25
  • 85
  • 135
Abdelhak ouaddi
  • 474
  • 2
  • 4
  • I started working with LexikJWTAuthenticationBundle, but wasn't not working for my code. I was getting a lot of errors, so finally decided to change it with Firebase JWT and do it manually, so I can adapt it to my code and avoid all that errors. I know it's not the most intuitive, but if it works, I'll be happy. – FourBars Oct 22 '21 at 11:30
  • Yes I see, it happens sometimes, good luck – Abdelhak ouaddi Oct 22 '21 at 11:35
1

Solution: Symfony 6

In my case, I came here looking for a Symfony 6 solution.

I had to install Firebase PHP SDK (composer require kreait/firebase-php)

I had to download authentication json file from firebase (Project configuration -> Service accounts) in order to init Firebase SDK

App\Security\JWTAuthenticator:

<?php
    namespace App\Security;
    
    use App\Entity\User;
    use Doctrine\ORM\EntityManagerInterface;
    use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
    use Symfony\Component\HttpFoundation\JsonResponse;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Security\Core\Exception\AuthenticationException;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Firebase\JWT\JWT;
    use Firebase\JWT\Key;
    use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
    use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
    use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
    use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
    use Symfony\Component\Cache\Simple\FilesystemCache;
    use Kreait\Firebase;
    use Firebase\Auth\Token\Exception\InvalidToken;
    
    
    class JWTAuthenticator extends AbstractAuthenticator
    {
        private $em;
        private $params;
        private $projectDirectory;
        private $firebase;
    
        public function __construct(string $projectDirectory, EntityManagerInterface $em, ContainerBagInterface $params)
        {
            $this->projectDirectory = $projectDirectory;
            $this->em = $em;
            $this->params = $params;
            $this->firebase = (new Firebase\Factory())->withServiceAccount($this->projectDirectory.'/firebase-authentication.json');
        }
    
        public function start(Request $request, AuthenticationException $authException = null): JsonResponse
        {
            $body = [
                'message' => 'Authentication Required',
            ];
            return new JsonResponse($body, Response::HTTP_UNAUTHORIZED);
        }
    
        public function supports(Request $request): bool
        {
            return $request->headers->has('Authorization');
        }
    
        public function getCredentials(Request $request)
        {
            return $request->headers->get('Authorization');
        }
    
        public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
        {
            return new JsonResponse([
                'message' => $exception->getMessage()
            ], Response::HTTP_UNAUTHORIZED);
        }
    
        public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
        {
            return null;
        }
    
        public function supportsRememberMe(): bool
        {
            return false;
        }
    
        public function authenticate(Request $request): Passport
        {
            try{
                $credentials = str_replace('Bearer ', '', $this->getCredentials($request));
                $firebaseAuth = $this->firebase->createAuth();
    
                try {
                    $verifiedIdToken = $firebaseAuth->verifyIdToken($credentials);
                    $tokenClaims = $verifiedIdToken->claims();

                    $sub = $tokenClaims->get('sub');
                    $email = $tokenClaims->get('email');
                    
                    if ($verifiedIdToken->isExpired(new \DateTime())) {
                        throw new AuthenticationException("Token expired.");
                    }
                    
                } catch (Firebase\Exception\AuthException | Firebase\Exception\FirebaseException $e) {
                    throw new AuthenticationException($e->getMessage());
                }
    
            }
            catch (\Exception $exception){
                throw new AuthenticationException($exception->getMessage());
            }
    
            return new SelfValidatingPassport(new UserBadge($email));
        }
    }

My security.yaml

security:
    enable_authenticator_manager: true
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
        App\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email


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

        main:
            lazy: true
            stateless: true
            provider: app_user_provider
            custom_authenticator: App\Security\JWTAuthenticator

    # 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: ^/api/docs, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }
Tomas
  • 866
  • 16
  • 21