I am developing a REST API with PHP 8.2, Symfony 6.4 and API Platform. I am finishing implementing OAuth with Google using the bundle KnpUOAuth2ClientBundle but I am getting the error that the title says. I have seen this problem in other threads but in my case and since it is an API, I do not have a front-end to redirect yet, but I don't know if that is really the problem.
I have divided my implementation into 3 classes: Client > Service > Controller. Below I leave the code and my configuration. (I will skip the class imports to not make it so long).
CLIENT
<?phpnamespace App\Http\Google;use League\OAuth2\Client\Provider\Exception\IdentityProviderException;use League\OAuth2\Client\Provider\Google;use League\OAuth2\Client\Token\AccessToken;class GoogleClient{ private Google $client; public function __construct( string $clientId, string $clientSecret) { $this->client = new Google(['clientId' => $clientId,'clientSecret' => $clientSecret,'redirectUri' => 'http://localhost:250/api/v1/users/google/oauth', ]); } /** * @throws IdentityProviderException */ public function getAccessToken(string $grant = 'authorization_code', array $options = []): AccessToken { return $this->client->getAccessToken($grant, $options); } public function getUserInfo(AccessToken $accessToken): array { $resourceOwner = $this->client->getResourceOwner($accessToken); return $resourceOwner->toArray(); }}
SERVICE
The "authorize" function is the function that is responsible for checking if a user exists in the database. If it does not exist, call to the function "createUser" and creates it, returns a JSON token with which to authenticate created with Lexik JWTAuthenticationBundle bundle. As I said before, I still no have a front-end to test the routes, so I am testing it using the URL provided by Google:
<?phpnamespace App\Service\Google;use App\Entity\User;use App\Exception\User\UserNotFoundException;use App\Http\Google\GoogleClient;use App\Repository\UserRepository;use App\Service\Password\EncoderService;use Doctrine\ORM\Exception\ORMException;use Doctrine\ORM\OptimisticLockException;use Exception;use KnpU\OAuth2ClientBundle\Client\ClientRegistry;use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;use Symfony\Component\Security\Core\Exception\AuthenticationException;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\Security\Http\EntryPoint\AuthenticationEntryPointInterface;class GoogleService extends OAuth2Authenticator implements AuthenticationEntrypointInterface{ private ClientRegistry $clientRegistry; private EncoderService $encoderService; private GoogleClient $googleClient; private JWTTokenManagerInterface $JWTTokenManager; private UserRepository $userRepository; public function __construct( ClientRegistry $clientRegistry, EncoderService $encoderService, GoogleClient $googleClient, JWTTokenManagerInterface $JWTTokenManager, UserRepository $userRepository ) { $this->clientRegistry = $clientRegistry; $this->encoderService = $encoderService; $this->googleClient = $googleClient; $this->JWTTokenManager = $JWTTokenManager; $this->userRepository = $userRepository; } /** * CUSTOM FUNCTIONS */ /** * @throws OptimisticLockException * @throws ORMException */ public function authorize(string $code): string { try { $accessToken = $this->googleClient->getAccessToken('authorization_code', ['code' => $code]); $userProfile = $this->googleClient->getUserInfo($accessToken); } catch (Exception $exception) { throw new BadRequestHttpException(sprintf('Google error. Message: %s', $exception->getMessage())); } $googleEmail = $userProfile['email'] ?? null; $googleName = $userProfile['name'] ?? null; if (null === $googleEmail) { throw new BadRequestHttpException('Google account without E-Mail.'); } try { $user = $this->userRepository->findOneByEmailOrFail($googleEmail); } catch (UserNotFoundException) { $user = $this->createUser($googleName, $googleEmail); } return $this->JWTTokenManager->create($user); } /** * @throws OptimisticLockException * @throws ORMException */ private function createUser(string $name, string $email): User { $user = new User($name, $email); $user->setPassword($this->encoderService->generateEncodedPassword($user, \sha1(\uniqid()))); $user->setActive(true); $user->setToken(null); $this->userRepository->save($user); return $user; } /** * OAUTH2AUTHENTICATOR FUNCTIONS */ public function supports(Request $request): ?bool { return $request->attributes->get('_route') === 'google_oauth'; } public function authenticate(Request $request): Passport { $client = $this->clientRegistry->getClient('google'); $accessToken = $this->fetchAccessToken($client); return new SelfValidatingPassport( new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) { return $client->fetchUserFromToken($accessToken); }) ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $message = strtr($exception->getMessageKey(), $exception->getMessageData()); return new Response( $message, Response::HTTP_INTERNAL_SERVER_ERROR ); } public function start(Request $request, AuthenticationException $authException = null): Response { return new Response('JWT required', Response::HTTP_UNAUTHORIZED ); }}
CONTROLLER
<?phpnamespace App\Controller\Action\Google;use App\Service\Google\GoogleService;use Doctrine\ORM\Exception\ORMException;use Symfony\Component\HttpFoundation\JsonResponse;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\HttpKernel\Attribute\AsController;#[AsController]class Authorization{ private GoogleService $googleService; public function __construct(GoogleService $googleService) { $this->googleService = $googleService; } public function __invoke(Request $request): JsonResponse { $code = $request->query->get('code'); if (!$code) { return new JsonResponse( ['error' => 'Authorization code not provided.'], Response::HTTP_FORBIDDEN ); } try { $token = $this->googleService->authorize($code); return new JsonResponse( ['token' => $token] ); } catch (ORMException $exception) { return new JsonResponse( ['error' => $exception->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR ); } }}
API ENDPOINT
google_oauth: class: ApiPlatform\Metadata\Get controller: App\Controller\Action\Google\Authorization deserialize: false method: GET uriTemplate: /users/google/oauth openapi: summary: OAuth with Google. requestBody: content: application/json: schema: type: object properties: code: type: string required: true responses: 200: description: OK content: application/json: schema: type: object properties: token: type: string
SECURITY.YAML
As specified in documentation.
security: enable_authenticator_manager: true google_oauth: pattern: ^/api/v1/users/google/oauth methods: [GET] custom_authenticators: - App\Service\Google\GoogleService
KNPU_OAUTH2_CLIENT.YAML
knpu_oauth2_client: clients: google: type: google client_id: '%env(OAUTH_GOOGLE_CLIENT_ID)%' client_secret: '%env(OAUTH_GOOGLE_CLIENT_SECRET)%' redirect_route: google_oauth redirect_params: {}
GOOGLE CLOUD CONSOLE ROUTES
- Authorized JavaScript Sources: http://localhost:250
- Authorized redirect URI's: http://localhost:250/api/v1/users/google/oauth
The truth is that I don't know what I'm doing wrong and I don't know where to look anymore. This is the first time I have implemented a system like this and I don't see where the error could be.
Thanks in advance.
I have tried to check the functionality with the URL that Google provides for debugging: https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=(MY ID).apps.googleusercontent.com&redirect_uri=http://localhost:250/api/v1/users/google/oauth&scope=email%20profile&access_type=offline&prompt=consent
I have changed and checked the routes in the Google Cloud Console. I have also tried doing "var_dump" at different points in the code but it does not return anything.
I have also tried testing in different browsers (Google Chrome, Opera and Firefox), but I get the same result in all of them.