Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
14 / 14 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
| GoogleAuthenticator | |
100.00% |
14 / 14 |
|
100.00% |
5 / 5 |
9 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| supports | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| authenticate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| onAuthenticationSuccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| onAuthenticationFailure | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace App\Identity\Infrastructure\Security; |
| 6 | |
| 7 | use App\Identity\Application\Command\SignInWithGoogleCommand; |
| 8 | use App\Identity\Application\Command\SignInWithGoogleHandler; |
| 9 | use App\Shared\Infrastructure\Http\ReadsJsonPayload; |
| 10 | use InvalidArgumentException; |
| 11 | use Symfony\Component\HttpFoundation\Request; |
| 12 | use Symfony\Component\HttpFoundation\Response; |
| 13 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
| 14 | use Symfony\Component\Security\Core\Exception\AuthenticationException; |
| 15 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; |
| 16 | use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; |
| 17 | use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; |
| 18 | use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; |
| 19 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; |
| 20 | use Symfony\Component\Security\Http\Authenticator\Passport\Passport; |
| 21 | use 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 | */ |
| 33 | final 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 | } |