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 | ||
| 3 | namespace App\Security; | |
| 4 | ||
| 5 | use App\Service\AvatarUpdater; | |
| 6 | use Symfony\Component\HttpFoundation\RedirectResponse; | |
| 7 | use Symfony\Component\HttpFoundation\Request; | |
| 8 | use Symfony\Component\HttpFoundation\Session\Session; | |
| 9 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
| 10 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
| 11 | use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
| 12 | use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |
| 13 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; | |
| 14 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
| 15 | use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
| 16 | use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; | |
| 17 | use Symfony\Component\Security\Http\Util\TargetPathTrait; | |
| 18 | ||
| 19 | class 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 | } |