Lines
89.65%
26 / 29
Functions and Methods
71.42%
5 / 7
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| GoogleApiTokenAuthenticator | 89.65% | 26 / 29 | 71.42% | 5 / 7 | 9.09 | 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 | 85.71% | 12 / 14 | 0.00% | 0 / 1 | 3.03 | |||||
| onAuthenticationSuccess | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| onAuthenticationFailure | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | |||||
| start | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| unauthorized | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 1 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace App\Security; | |
| 6 | ||
| 7 | use App\Entity\User; | |
| 8 | use App\Service\AvatarUpdater; | |
| 9 | use Symfony\Component\HttpFoundation\JsonResponse; | |
| 10 | use Symfony\Component\HttpFoundation\Request; | |
| 11 | use Symfony\Component\HttpFoundation\Response; | |
| 12 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
| 13 | use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
| 14 | use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | |
| 15 | use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |
| 16 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
| 17 | use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
| 18 | use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; | |
| 19 | use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; | |
| 20 | ||
| 21 | /** | |
| 22 | * Authenticates API requests carrying a Google ID token as a bearer token. | |
| 23 | * | |
| 24 | * Unknown Google accounts are registered, like a Google login on the web: with ROLE_USER, so | |
| 25 | * they see no agent stats until an admin makes them agents. | |
| 26 | */ | |
| 27 | class GoogleApiTokenAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface | |
| 28 | { | |
| 29 | private const string BEARER_PREFIX = 'Bearer '; | |
| 30 | ||
| 31 | public function __construct( | |
| 32 | private readonly GoogleIdTokenVerifierInterface $idTokenVerifier, | |
| 33 | private readonly GoogleUserResolver $userResolver, | |
| 34 | private readonly AvatarUpdater $avatarUpdater, | |
| 35 | ) { | |
| 36 | } | |
| 37 | ||
| 38 | #[\Override] | |
| 39 | public function supports(Request $request): bool | |
| 40 | { | |
| 41 | return \str_starts_with((string) $request->headers->get('Authorization'), self::BEARER_PREFIX); | |
| 42 | } | |
| 43 | ||
| 44 | #[\Override] | |
| 45 | public function authenticate(Request $request): Passport | |
| 46 | { | |
| 47 | $idToken = \trim(\substr((string) $request->headers->get('Authorization'), \strlen(self::BEARER_PREFIX))); | |
| 48 | ||
| 49 | if ('' === $idToken) { | |
| 50 | throw new CustomUserMessageAuthenticationException('Missing ID token.'); | |
| 51 | } | |
| 52 | ||
| 53 | $payload = $this->idTokenVerifier->verify($idToken); | |
| 54 | ||
| 55 | if (null === $payload) { | |
| 56 | throw new CustomUserMessageAuthenticationException('Invalid ID token.'); | |
| 57 | } | |
| 58 | ||
| 59 | [$googleId, $email] = GoogleUserResolver::identity($payload); | |
| 60 | ||
| 61 | return new SelfValidatingPassport( | |
| 62 | new UserBadge($googleId, function () use ($googleId, $email, $payload): User { | |
| 63 | $user = $this->userResolver->resolve($googleId, $email); | |
| 64 | // The app signs in with Google only: its users get their picture here | |
| 65 | $this->avatarUpdater->update($user, $payload['picture'] ?? null); | |
| 66 | ||
| 67 | return $user; | |
| 68 | }) | |
| 69 | ); | |
| 70 | } | |
| 71 | ||
| 72 | #[\Override] | |
| 73 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
| 74 | { | |
| 75 | return null; | |
| 76 | } | |
| 77 | ||
| 78 | #[\Override] | |
| 79 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse | |
| 80 | { | |
| 81 | return $this->unauthorized(\strtr($exception->getMessageKey(), $exception->getMessageData())); | |
| 82 | } | |
| 83 | ||
| 84 | #[\Override] | |
| 85 | public function start(Request $request, ?AuthenticationException $authException = null): JsonResponse | |
| 86 | { | |
| 87 | return $this->unauthorized('Authentication required.'); | |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * An RFC 7807 problem, like API Platform's own errors. | |
| 92 | */ | |
| 93 | private function unauthorized(string $message): JsonResponse | |
| 94 | { | |
| 95 | return new JsonResponse( | |
| 96 | [ | |
| 97 | 'type' => '/errors/401', | |
| 98 | 'title' => 'An error occurred', | |
| 99 | 'status' => Response::HTTP_UNAUTHORIZED, | |
| 100 | 'detail' => $message, | |
| 101 | ], | |
| 102 | Response::HTTP_UNAUTHORIZED, | |
| 103 | ['WWW-Authenticate' => 'Bearer', 'Content-Type' => 'application/problem+json'], | |
| 104 | ); | |
| 105 | } | |
| 106 | } |