Lines 96.96% 32 / 33
Functions and Methods 80.00% 4 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
GoogleIdentityAuthenticator 96.96% 32 / 33 80.00% 4 / 5 11 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 supports 100.00% 1 / 1 100.00% 1 / 1 1
 authenticate 94.44% 17 / 18 0.00% 0 / 1 6.01
 onAuthenticationSuccess 100.00% 6 / 6 100.00% 1 / 1 2
 onAuthenticationFailure 100.00% 7 / 7 100.00% 1 / 1 1
1<?php
2
3namespace App\Security;
4
5use App\Service\AvatarUpdater;
6use Symfony\Component\HttpFoundation\RedirectResponse;
7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpFoundation\Session\Session;
9use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
10use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
11use Symfony\Component\Security\Core\Exception\AuthenticationException;
12use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
13use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
14use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
15use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
16use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
17use Symfony\Component\Security\Http\Util\TargetPathTrait;
18
19class GoogleIdentityAuthenticator extends AbstractAuthenticator
20{
21    use TargetPathTrait;
22
23    public const string CSRF_TOKEN = 'g_csrf_token';
24
25    public function __construct(
26        private readonly GoogleIdTokenVerifierInterface $idTokenVerifier,
27        private readonly GoogleUserResolver $userResolver,
28        private readonly UrlGeneratorInterface $urlGenerator,
29        private readonly ApiTokenStore $apiTokenStore,
30        private readonly AvatarUpdater $avatarUpdater,
31    ) {
32    }
33
34    #[\Override]
35    public function supports(Request $request): bool
36    {
37        return '/connect/google/verify' === $request->getPathInfo();
38    }
39
40    #[\Override]
41    public function authenticate(Request $request): Passport
42    {
43        // Google's double-submit cookie: the button posts the same g_csrf_token as a cookie and a field
44        $csrfCookie = (string) $request->cookies->get(self::CSRF_TOKEN);
45        $csrfField = (string) $request->request->get(self::CSRF_TOKEN);
46
47        if ('' === $csrfCookie || !\hash_equals($csrfCookie, $csrfField)) {
48            throw new AuthenticationException('Invalid CSRF token :(');
49        }
50
51        $idToken = (string) $request->request->get('credential');
52
53        if ('' === $idToken || '0' === $idToken) {
54            throw new AuthenticationException('Missing credentials :(');
55        }
56
57        $payload = $this->idTokenVerifier->verify($idToken);
58
59        if (null === $payload) {
60            throw new AuthenticationException('Invalid ID token :(');
61        }
62
63        [$googleId, $email] = GoogleUserResolver::identity($payload);
64
65        $user = $this->userResolver->resolve($googleId, $email);
66        $this->avatarUpdater->update($user, $payload['picture'] ?? null);
67        $this->apiTokenStore->store($request->getSession(), $idToken, $payload);
68
69        return new SelfValidatingPassport(
70            new UserBadge($user->getUserIdentifier()),
71            [new RememberMeBadge()],
72        );
73    }
74
75    #[\Override]
76    public function onAuthenticationSuccess(
77        Request $request,
78        TokenInterface $token,
79        string $firewallName
80    ): RedirectResponse {
81        if ($targetPath = $this->getTargetPath(
82            $request->getSession(),
83            $firewallName
84        )
85        ) {
86            return new RedirectResponse($targetPath);
87        }
88
89        return new RedirectResponse($this->urlGenerator->generate('app_default'));
90    }
91
92    #[\Override]
93    public function onAuthenticationFailure(
94        Request $request,
95        AuthenticationException $exception
96    ): RedirectResponse {
97        $message = \strtr(
98            $exception->getMessageKey(),
99            $exception->getMessageData()
100        );
101
102        /**
103         * @var Session $session
104         */
105        $session = $request->getSession();
106        $session->getFlashBag()->add('danger', $message);
107
108        return new RedirectResponse($this->urlGenerator->generate('app_login'));
109    }
110}