Skip to content

Commit

Permalink
- Initial VAX Implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
lmelnyk-va committed Jul 4, 2018
1 parent 18209c6 commit ca36c03
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/Auth/FetchAuthToken.php
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;
}
53 changes: 53 additions & 0 deletions src/Auth/FetchAuthTokenCache.php
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;
}
}
137 changes: 137 additions & 0 deletions src/Auth/FetchVendastaAuthToken.php
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)
)
);
}
}
27 changes: 27 additions & 0 deletions src/Environment.php
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";
}
84 changes: 84 additions & 0 deletions src/GRPCClient.php
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;
}
}
Loading

0 comments on commit ca36c03

Please sign in to comment.