-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
18209c6
commit ca36c03
Showing
9 changed files
with
541 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<?php | ||
namespace Vendasta\Vax\Auth; | ||
|
||
interface FetchAuthToken { | ||
public function fetchToken(): string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
namespace Vendasta\Vax\Auth; | ||
|
||
use Exception; | ||
|
||
class FetchAuthTokenCache implements FetchAuthToken | ||
{ | ||
private $fetcher; | ||
private $token; | ||
private $tokenExpiry; | ||
|
||
public function __construct(FetchAuthToken $fetcher) | ||
{ | ||
$this->fetcher = $fetcher; | ||
} | ||
|
||
public function fetchToken(): string | ||
{ | ||
$now = time(); | ||
if ($this->token == null || ($this->tokenExpiry != null && $this->tokenExpiry < $now)) { | ||
$this->token = $this->fetcher->fetchToken(); | ||
$this->tokenExpiry = self::parseExpiry($this->token); | ||
} | ||
|
||
if ($this->token == null) { | ||
throw new Exception("Could not refresh token"); | ||
} | ||
|
||
return $this->token; | ||
} | ||
|
||
public function invalidateToken() | ||
{ | ||
$this->token = null; | ||
$this->tokenExpiry = null; | ||
} | ||
|
||
private static function parseExpiry(string $token): ?int | ||
{ | ||
if ($token == null) { | ||
return null; | ||
} | ||
|
||
$jwt_parts = explode(".", $token); | ||
if (sizeof($jwt_parts) !== 3) { | ||
return null; | ||
} | ||
|
||
$claims = json_decode(base64_decode($jwt_parts[1])); | ||
return $claims->exp; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
<?php | ||
|
||
namespace Vendasta\Vax\Auth; | ||
|
||
use GuzzleHttp\Client; | ||
use GuzzleHttp\Exception\GuzzleException; | ||
use Mdanter\Ecc\Crypto\Signature\SignatureInterface; | ||
use Mdanter\Ecc\Crypto\Signature\Signer; | ||
use Mdanter\Ecc\Crypto\Signature\SignHasher; | ||
use Mdanter\Ecc\EccFactory; | ||
use Mdanter\Ecc\Math\GmpMathInterface; | ||
use Mdanter\Ecc\Random\RandomGeneratorFactory; | ||
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer; | ||
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer; | ||
use Vendasta\Vax\Environment; | ||
|
||
class FetchVendastaAuthToken implements FetchAuthToken | ||
{ | ||
private $token_uri; | ||
private $key; | ||
private $email; | ||
private $key_id; | ||
private $client; | ||
|
||
public function __construct(string $scope) | ||
{ | ||
$jsonKey = getenv("VENDASTA_APPLICATION_CREDENTIALS"); | ||
if (is_string($jsonKey)) { | ||
if (!file_exists($jsonKey)) { | ||
throw new \InvalidArgumentException('file does not exist'); | ||
} | ||
$jsonKeyStream = file_get_contents($jsonKey); | ||
if (!$jsonKey = json_decode($jsonKeyStream, true)) { | ||
throw new \LogicException('invalid json for auth config'); | ||
} | ||
} else { | ||
throw new \InvalidArgumentException('VENDASTA_APPLICATION_CREDENTIALS not set'); | ||
} | ||
|
||
if (!array_key_exists('client_email', $jsonKey)) { | ||
throw new \InvalidArgumentException( | ||
'json key is missing the client_email field'); | ||
} | ||
if (!array_key_exists('private_key', $jsonKey)) { | ||
throw new \InvalidArgumentException( | ||
'json key is missing the private_key field'); | ||
} | ||
|
||
$this->token_uri = $jsonKey['token_uri']; | ||
$this->key = $jsonKey['private_key']; | ||
$this->email = $jsonKey['client_email']; | ||
$this->key_id = $jsonKey['private_key_id']; | ||
|
||
$this->client = new Client([ | ||
'timeout' => 5, | ||
]); | ||
} | ||
|
||
public function fetchToken(): string | ||
{ | ||
$token = $this->buildJWT(); | ||
|
||
$response = null; | ||
try { | ||
$response = $this->client->request( | ||
'POST', | ||
$this->token_uri, | ||
[ | ||
'json' => [ | ||
'token' => sprintf('%s', $token), | ||
], | ||
] | ||
); | ||
} catch (GuzzleException $e) { | ||
// Handle this exception | ||
} | ||
if ($response == null) { | ||
return null; | ||
} | ||
$body = (string)$response->getBody(); | ||
$json_body = json_decode($body); | ||
return $json_body->token; | ||
} | ||
|
||
private function buildJWT() | ||
{ | ||
$now = time(); | ||
|
||
$header = [ | ||
'typ' => 'JWT', | ||
'alg' => 'ES256', | ||
]; | ||
|
||
$token = [ | ||
'sub' => $this->email, | ||
'aud' => 'vendasta.com', | ||
'iat' => $now, | ||
'exp' => $now + 3600, | ||
'kid' => $this->key_id, | ||
]; | ||
|
||
$adapter = EccFactory::getAdapter(); | ||
$generator = EccFactory::getNistCurves()->generator256(); | ||
$algorithm = 'sha256'; | ||
$document = self::encode(json_encode($header, true)) . '.' . self::encode(json_encode($token)); | ||
$pemSerializer = new PemPrivateKeySerializer(new DerPrivateKeySerializer($adapter)); | ||
$key = $pemSerializer->parse($this->key); | ||
$hasher = new SignHasher($algorithm, $adapter); | ||
$hash = $hasher->makeHash($document, $generator); | ||
$random = RandomGeneratorFactory::getRandomGenerator(); | ||
$randomK = $random->generate($generator->getOrder()); | ||
$signer = new Signer($adapter); | ||
$signature = $signer->sign($key, $hash, $randomK); | ||
|
||
$signed = $document . "." . self::encode(self::createSignatureHash($signature, $adapter)); | ||
return $signed; | ||
} | ||
|
||
private static function encode($data, bool $use_padding = false) | ||
{ | ||
$encoded = strtr(base64_encode($data), '+/', '-_'); | ||
return true === $use_padding ? $encoded : rtrim($encoded, '='); | ||
} | ||
|
||
private static function createSignatureHash(SignatureInterface $signature, GmpMathInterface $adapter) | ||
{ | ||
$length = 64; | ||
return pack( | ||
'H*', | ||
sprintf( | ||
'%s%s', | ||
str_pad($adapter->decHex($signature->getR()), $length, '0', STR_PAD_LEFT), | ||
str_pad($adapter->decHex($signature->getS()), $length, '0', STR_PAD_LEFT) | ||
) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
|
||
namespace Vendasta\Vax; | ||
|
||
|
||
abstract class Environment | ||
{ | ||
/** | ||
* Local environment | ||
*/ | ||
const LOCAL = "LOCAL"; | ||
|
||
/** | ||
* Test environment | ||
*/ | ||
const TEST = "TEST"; | ||
|
||
/** | ||
* Demo environment | ||
*/ | ||
const DEMO = "DEMO"; | ||
|
||
/** | ||
* Prod environment | ||
*/ | ||
const PROD = "PROD"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<?php | ||
|
||
namespace Vendasta\Vax; | ||
|
||
use Google\Protobuf\Internal\Message; | ||
use Grpc\Channel; | ||
use Grpc\CallCredentials; | ||
use Grpc\ChannelCredentials; | ||
use Vendasta\Vax\Auth\FetchAuthTokenCache; | ||
use Vendasta\Vax\Auth\FetchVendastaAuthToken; | ||
|
||
|
||
/** | ||
* Class GRPCClient | ||
* @package Vendasta\Accounts\V1\Vax | ||
* | ||
* Base GRPCClient class which adds authorization to all outbound grpc requests | ||
*/ | ||
class GRPCClient extends VAXClient | ||
{ | ||
private $auth; | ||
private $secure; | ||
|
||
/** | ||
* JSONClient constructor. | ||
* @param string $host | ||
* @param string $scope | ||
* @param bool $secure | ||
* @param float $default_timeout | ||
*/ | ||
public function __construct(string $host, string $scope, bool $secure = true, float $default_timeout = 10000) | ||
{ | ||
parent::__construct($default_timeout); | ||
$this->auth = new FetchAuthTokenCache(new FetchVendastaAuthToken($scope)); | ||
$this->secure = $secure; | ||
} | ||
|
||
protected function getClientOptions(): array { | ||
return [ | ||
'credentials' => ($this->secure ? ChannelCredentials::createSsl() : ChannelCredentials::createInsecure()), | ||
]; | ||
} | ||
|
||
private function buildGRPCOptions(array $options = []): array | ||
{ | ||
$opts = $this->buildVAXOptions($options); | ||
$grpcOpts = [ | ||
'timeout' => $opts->timeout * 1000 // microseconds, | ||
]; | ||
|
||
if ($opts->include_token) { | ||
$auth = $this->auth; | ||
$grpcOpts['call_credentials_callback'] = function() use ($auth) { | ||
return ['authorization' => ['Bearer ' . $auth->fetchToken()]]; | ||
}; | ||
} | ||
return $grpcOpts; | ||
} | ||
|
||
/** | ||
* @param callable $client_call | ||
* @param Message $req | ||
* @param array $options possible keys: | ||
* \Vendasta\Vax\RequestOptions::* | ||
* @throws SDKException on failed call | ||
* @return Message | ||
*/ | ||
protected function doRequest(callable $client_call, Message $req, array $options = []) | ||
{ | ||
list($response, $status) = $client_call($req, [], $this->buildGRPCOptions($options))->wait(); | ||
if ($status->code) { | ||
if ($status->code == 16) { | ||
$this->auth->invalidateToken(); | ||
list($response, $status) = $client_call($req, [], $this->buildGRPCOptions($options))->wait(); | ||
if ($status->code) { | ||
throw new SDKException($status->details, $status->code); | ||
} | ||
} else { | ||
throw new SDKException($status->details, $status->code); | ||
} | ||
} | ||
return $response; | ||
} | ||
} |
Oops, something went wrong.