From fb31574c0cd7c68a55c3c0ee10ff84e2c87d5f8d Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Fri, 2 Jun 2023 00:30:32 +0900 Subject: [PATCH] feat: Allow insecure fields in optional --- src/Claims.php | 49 ++++++++++++---------- src/GoogleIdTokenVerifier.php | 2 +- src/Http/GoogleIapGuard.php | 26 +++++++++++- src/Internal/Assert.php | 28 +++++++++++++ src/Providers/GoogleIapServiceProvider.php | 8 +++- 5 files changed, 87 insertions(+), 26 deletions(-) diff --git a/src/Claims.php b/src/Claims.php index d3f64da..b389747 100644 --- a/src/Claims.php +++ b/src/Claims.php @@ -10,7 +10,7 @@ class Claims { /** - * @var array{ + * @param array{ * exp: positive-int, * iat: positive-int, * aud: non-empty-string, @@ -18,33 +18,13 @@ class Claims * hd: non-empty-string, * sub: non-empty-string, * email: non-empty-string, - * } + * } $claims * * @see https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload */ - public readonly array $claims; - - /** - * @param array $claims - * - * @throws MalformedClaimsException - */ public function __construct( - array $claims, + public readonly array $claims, ) { - try { - $this->claims = [ - 'exp' => Assert::positiveInt(Assert::in('exp', $claims)), - 'iat' => Assert::positiveInt(Assert::in('iat', $claims)), - 'aud' => Assert::nonEmptyString(Assert::in('aud', $claims)), - 'iss' => Assert::nonEmptyString(Assert::in('iss', $claims)), - 'hd' => Assert::nonEmptyString(Assert::in('hd', $claims)), - 'sub' => Assert::nonEmptyString(Assert::in('sub', $claims)), - 'email' => Assert::nonEmptyString(Assert::in('email', $claims)), - ]; - } catch (AssertionException $e) { - throw new MalformedClaimsException($e); - } } /** @@ -141,4 +121,27 @@ public function id(): string throw new MalformedClaimsException($e); } } + + /** + * @param array $claims + * + * @throws MalformedClaimsException + */ + public static function from( + array $claims, + ): self { + try { + return new self([ + 'exp' => Assert::positiveInt(Assert::in('exp', $claims)), + 'iat' => Assert::positiveInt(Assert::in('iat', $claims)), + 'aud' => Assert::nonEmptyString(Assert::in('aud', $claims)), + 'iss' => Assert::nonEmptyString(Assert::in('iss', $claims)), + 'hd' => Assert::nonEmptyString(Assert::in('hd', $claims)), + 'sub' => Assert::nonEmptyString(Assert::in('sub', $claims)), + 'email' => Assert::nonEmptyString(Assert::in('email', $claims)), + ]); + } catch (AssertionException $e) { + throw new MalformedClaimsException($e); + } + } } diff --git a/src/GoogleIdTokenVerifier.php b/src/GoogleIdTokenVerifier.php index f5e0f70..386e175 100644 --- a/src/GoogleIdTokenVerifier.php +++ b/src/GoogleIdTokenVerifier.php @@ -37,7 +37,7 @@ public function verify(string $jwt): ?Claims return null; } - $claims = new Claims($claims); + $claims = Claims::from($claims); if ($this->issuer !== null && $claims->iss() !== $this->issuer) { // Issuer verification failed. diff --git a/src/Http/GoogleIapGuard.php b/src/Http/GoogleIapGuard.php index f2d2b4d..f0de3ec 100644 --- a/src/Http/GoogleIapGuard.php +++ b/src/Http/GoogleIapGuard.php @@ -11,14 +11,20 @@ use YumemiInc\GoogleIapLaravel\DefaultGoogleUserResolver; use YumemiInc\GoogleIapLaravel\GoogleIdTokenVerifier; use YumemiInc\GoogleIapLaravel\GoogleUserResolver; +use YumemiInc\GoogleIapLaravel\Internal\Assert; +use YumemiInc\GoogleIapLaravel\Internal\AssertionException; use YumemiInc\GoogleIapLaravel\MalformedClaimsException; class GoogleIapGuard extends RequestGuard { + /** + * @param array{allow_insecure_headers?: bool} $options + */ public function __construct( Request $request, private readonly GoogleIdTokenVerifier $googleIdTokenVerifier = new GoogleIdTokenVerifier(), private readonly GoogleUserResolver $userProviderAdapter = new DefaultGoogleUserResolver(), + private readonly array $options = [], ) { parent::__construct(static::callback(...), $request); } @@ -33,7 +39,25 @@ public function callback(): ?Authenticatable return null; } - if (!($claims = $this->googleIdTokenVerifier->verify($jwt)) instanceof Claims) { + try { + $id = Assert::nonEmptyStringOrNull($this->request->header('x-goog-authenticated-user-id')); + $email = Assert::nonEmptyStringOrNull($this->request->header('x-goog-authenticated-user-email')); + $hd = ($email === null ? null : Assert::nonEmptyString(explode('@', $email)[1])) ?? 'example.com'; + } catch (AssertionException $e) { + throw new MalformedClaimsException($e); + } + + if (($this->options['allow_insecure_headers'] ?? false) && ($id !== null || $email !== null)) { + $claims = new Claims([ + 'exp' => \PHP_INT_MAX, + 'iat' => 1, + 'aud' => 'insecure', + 'iss' => 'https://cloud.google.com/iap', + 'hd' => $hd, + 'sub' => $id ?? 'accounts.google.com:0', + 'email' => $email ?? 'accounts.google.com:insecure@example.com', + ]); + } elseif (!($claims = $this->googleIdTokenVerifier->verify($jwt)) instanceof Claims) { return null; } diff --git a/src/Internal/Assert.php b/src/Internal/Assert.php index e74586b..5a081aa 100644 --- a/src/Internal/Assert.php +++ b/src/Internal/Assert.php @@ -62,6 +62,20 @@ public static function nonEmptyString(mixed $value): string return $s; } + /** + * @phpstan-return ($value is non-empty-string ? non-empty-string : ($value is null ? null : never)) + * + * @throws AssertionException + */ + public static function nonEmptyStringOrNull(mixed $value): null|string + { + try { + return self::nonEmptyString($value); + } catch (AssertionException) { + return self::null($value); + } + } + /** * @template T * @@ -80,6 +94,20 @@ public static function nonNull(mixed $value): mixed return $value; } + /** + * @phpstan-return ($value is null ? null : never) + * + * @throws AssertionException + */ + public static function null(mixed $value): mixed + { + if ($value !== null) { + throw new AssertionException('null', 'non-null value'); + } + + return null; + } + /** * @template TKey of array-key * @template TValue diff --git a/src/Providers/GoogleIapServiceProvider.php b/src/Providers/GoogleIapServiceProvider.php index 9f4b42d..e42107a 100644 --- a/src/Providers/GoogleIapServiceProvider.php +++ b/src/Providers/GoogleIapServiceProvider.php @@ -35,7 +35,13 @@ protected function extendComponents(AuthManager $auth): void $auth->extend( 'google-iap', function (Container $app, string $name, array $config) use ($auth): GoogleIapGuard { - $guard = $this->app->make(GoogleIapGuard::class); + $guard = new GoogleIapGuard( + $app->make('request'), + $app->make(GoogleIdTokenVerifier::class), + $app->make(GoogleUserResolver::class), + $config['options'] ?? [], + ); + $guard->setProvider(Assert::nonNull($auth->createUserProvider($config['provider'] ?? null))); return $guard;