Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
GoogleAuthenticator
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
5 / 5
9
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 supports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 authenticate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 onAuthenticationSuccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onAuthenticationFailure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Identity\Infrastructure\Security;
6
7use App\Identity\Application\Command\SignInWithGoogleCommand;
8use App\Identity\Application\Command\SignInWithGoogleHandler;
9use App\Shared\Infrastructure\Http\ReadsJsonPayload;
10use InvalidArgumentException;
11use Symfony\Component\HttpFoundation\Request;
12use Symfony\Component\HttpFoundation\Response;
13use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
14use Symfony\Component\Security\Core\Exception\AuthenticationException;
15use Symfony\Component\Security\Core\Exception\BadCredentialsException;
16use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
17use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
18use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
19use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
20use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
21use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
22
23/**
24 * Signs a user in from a Google ID token (GIS button, no redirect).
25 *
26 * An authenticator on the same firewall as json_login rather than a controller,
27 * because everything after "this user is who they claim" is then shared for
28 * free: lexik signs the JWT in its success handler, gesdinet's global
29 * AUTHENTICATION_SUCCESS listener attaches the rotating httpOnly refresh cookie,
30 * and UserChecker + login_throttling are firewall-wide. The response is
31 * therefore identical in shape to POST /login.
32 */
33final class GoogleAuthenticator extends AbstractAuthenticator
34{
35    use ReadsJsonPayload;
36
37    public const CHECK_PATH = '/login/google';
38
39    public function __construct(
40        private readonly SignInWithGoogleHandler $handler,
41        private readonly AuthenticationSuccessHandlerInterface $successHandler,
42        private readonly AuthenticationFailureHandlerInterface $failureHandler,
43    ) {
44    }
45
46    public function supports(Request $request): bool
47    {
48        return $request->isMethod('POST') && $request->getPathInfo() === self::CHECK_PATH;
49    }
50
51    public function authenticate(Request $request): Passport
52    {
53        try {
54            $credential = $this->stringField($this->decodeJsonBody($request), 'credential');
55        } catch (InvalidArgumentException) {
56            throw new BadCredentialsException();
57        }
58
59        $user = $credential === '' ? null : $this->handler->handle(new SignInWithGoogleCommand($credential));
60
61        if ($user === null) {
62            throw new BadCredentialsException();
63        }
64
65        $securityUser = SecurityUser::fromDomain($user);
66
67        // Self-validating: the ID token was the credential and it is already
68        // verified. The closure hands back the user we just loaded instead of
69        // making the provider re-read it, while UserChecker still runs on it.
70        return new SelfValidatingPassport(
71            new UserBadge($securityUser->getUserIdentifier(), static fn (): SecurityUser => $securityUser),
72        );
73    }
74
75    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
76    {
77        return $this->successHandler->onAuthenticationSuccess($request, $token);
78    }
79
80    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
81    {
82        return $this->failureHandler->onAuthenticationFailure($request, $exception);
83    }
84}