I’m following all the steps to verify the Google Authentication api login according to the documentation in the link below:
https://developers.google.com/identity/gsi/web/guides/verify-google-id-token
And according to the documentation, it says to use the Google APIs Client Library for PHP
https://github.com/googleapis/google-api-php-client
But this library in composer loads more than 20 thousand files. And my server has a limit of inodes (amount of files that can exist on the hosting) So I think it would be too heavy to for check the login certificate token.
And then in the library I saw that there is a line to clean up unused services with GoogleTaskComposer::cleanup
by putting the following lines in composer:
{
"require": {
"google/apiclient": "^2.12.1"
},
"scripts": {
"pre-autoload-dump": "Google\Task\Composer::cleanup"
},
"extra": {
"google/apiclient-services": [
"Drive",
"YouTube"
]
}
}
But I’m not going to use any of the services other than the auth token verification. How should I put these lines?
I also thought about not using any of that and just putting firebase/php-jwt
in the composer and trying to recreate the library’s Verify.php file
But I still don’t know how to do it, it seems that a file always depends on other files and then the composer ends up still loading hundreds of useless files.
I’ve been trying to do this for days, and I can’t find any solution that doesn’t need to load so many files. Does anyone know how to do this more directly, with only what is really needed?
This is the file I’m trying to rebuild, but everything is still weird and I’m not understanding if I’m on the right path.
<?php
namespace FVRCmds;
use FirebaseJWTExpiredException as ExpiredExceptionV3;
use FirebaseJWTKey;
use FirebaseJWTSignatureInvalidException;
class FVRJWT {
const FEDERATED_SIGNON_CERT_URL = 'https://www.googleapis.com/oauth2/v3/certs';
const OAUTH2_ISSUER = 'accounts.google.com';
const OAUTH2_ISSUER_HTTPS = 'https://accounts.google.com';
/**
* @var FirebaseJWTJWT
*/
public $jwt;
/**
* Instantiates the class, but does not initiate the login flow, leaving it
* to the discretion of the caller.
*/
public function __construct($jwt = null) {
$this->jwt = $jwt ?: $this->getJwtService();
}
/**
* Verifies an id token and returns the authenticated apiLoginTicket.
* Throws an exception if the id token is not valid.
* The audience parameter can be used to control which id tokens are
* accepted. By default, the id token must have been issued to this OAuth2 client.
*
* @param string $idToken the ID token in JWT format
* @param string $audience Optional. The audience to verify against JWt "aud"
* @return array|false the token payload, if successful
*/
public function verifyIdToken($idToken, $audience = null)
{
if (empty($idToken)) {
throw new LogicException('id_token cannot be null');
}
// set phpseclib constants if applicable
$this->setPhpsecConstants();
// Check signature
$certs = $this->getFederatedSignOnCerts();
foreach ($certs as $cert) {
try {
$args = [$idToken];
$publicKey = $this->getPublicKey($cert);
if (class_exists(Key::class)) {
$args[] = new Key($publicKey, 'RS256');
} else {
$args[] = $publicKey;
$args[] = ['RS256'];
}
$payload = call_user_func_array([$this->jwt, 'decode'], $args);
if (property_exists($payload, 'aud')) {
if ($audience && $payload->aud != $audience) {
return false;
}
}
// support HTTP and HTTPS issuers
// @see https://developers.google.com/identity/sign-in/web/backend-auth
$issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
return false;
}
return (array) $payload;
} catch (ExpiredException $e) { // @phpstan-ignore-line
return false;
} catch (ExpiredExceptionV3 $e) {
return false;
} catch (SignatureInvalidException $e) {
// continue
} catch (DomainException $e) {
// continue
}
}
return false;
}
private function getCache()
{
return $this->cache;
}
/**
* Retrieve a certificates file.
*
* @param string $url location
* @return array certificates
*/
private function retrieveCertsFromLocation($url)
{
if (!$file = file_get_contents($url)) {
throw new Exception(
"Failed to retrieve verification certificates: '" .
$url . "'."
);
}
return json_decode($file, true);
}
// Gets federated sign-on certificates to use for verifying identity tokens.
// Returns certs as array structure, where keys are key ids, and values
// are PEM encoded certificates.
private function getFederatedSignOnCerts()
{
$certs = $this->retrieveCertsFromLocation(
self::FEDERATED_SIGNON_CERT_URL
);
if (!isset($certs['keys'])) {
throw new Exception(
'federated sign-on certs expects "keys" to be set'
);
}
return $certs['keys'];
}
private function getJwtService()
{
$jwtClass = 'JWT';
if (class_exists('FirebaseJWTJWT')) {
$jwtClass = 'FirebaseJWTJWT';
}
return new $jwtClass();
}
private function getPublicKey($cert)
{
$modulus = $this->jwt->urlsafeB64Decode($cert['n']);
$exponent = $this->jwt->urlsafeB64Decode($cert['e']);
$component = ['n' => $modulus, 'e' => $exponent];
if (class_exists('phpseclib3CryptRSAPublicKey')) {
/** @var PublicKey $loader */
$loader = PublicKeyLoader::load($component);
return $loader->toString('PKCS8');
}
$rsaClass = $this->getRsaClass();
$rsa = new $rsaClass();
$rsa->loadKey($component);
return $rsa->getPublicKey();
}
private function getRsaClass()
{
if (class_exists('phpseclib3CryptRSA')) {
return 'phpseclib3CryptRSA';
}
if (class_exists('phpseclibCryptRSA')) {
return 'phpseclibCryptRSA';
}
return 'Crypt_RSA';
}
private function getOpenSslConstant()
{
if (class_exists('phpseclib3CryptAES')) {
return 'phpseclib3CryptAES::ENGINE_OPENSSL';
}
if (class_exists('phpseclibCryptRSA')) {
return 'phpseclibCryptRSA::MODE_OPENSSL';
}
if (class_exists('Crypt_RSA')) {
return 'CRYPT_RSA_MODE_OPENSSL';
}
throw new Exception('Cannot find RSA class');
}
/**
* phpseclib calls "phpinfo" by default, which requires special
* whitelisting in the AppEngine VM environment. This function
* sets constants to bypass the need for phpseclib to check phpinfo
*
* @see phpseclib/Math/BigInteger
* @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
*/
private function setPhpsecConstants()
{
if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) {
if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) {
define('MATH_BIGINTEGER_OPENSSL_ENABLED', true);
}
if (!defined('CRYPT_RSA_MODE')) {
define('CRYPT_RSA_MODE', constant($this->getOpenSslConstant()));
}
}
}
}
2
Answers
(similar SO question has some more details too)
for no services at all (for instance you’re only using the API to verify the integrity of a JWT ID token) the
extra.google/apiclient-services
array/list incomposer.json
needs to contain an empty stringbefore running
– That clears up ~20000 files
(general task described at Google API client README)
The PHP client for Google, there are almost 20,000 files and more than 50 Megabytes. Just monstrous, if you just want to validate a user.
Use one of these access points.
If all goes well, the result of $response will be an array with the user’s basic information.
Then you can add the user to the database, set cookies, etc.
This way of doing it, does not need Guzzle, no Google PHP SDK, no JWT.
It’s only 10 lines of code. Very simple.