Skip to content

Commit

Permalink
add native auth ttl check + refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
michavie committed Feb 27, 2023
1 parent ee43c75 commit 801d104
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 18 deletions.
29 changes: 22 additions & 7 deletions src/Auth/NativeAuthServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
use Peerme\Mx\UserVerifier;
use Peerme\MxLaravel\Auth\NativeAuthDecoded;
use Peerme\MxLaravel\Auth\NativeAuthValidateResult;
use Peerme\MxLaravel\Exceptions\NativeAuthInvalidSignatureException;
use Peerme\MxLaravel\Exceptions\NativeAuthInvalidTokenTtlException;
use Peerme\MxLaravel\Exceptions\NativeAuthOriginNotAcceptedException;
use Peerme\MxLaravel\Exceptions\NativeAuthTokenExpiredException;

class NativeAuthServer
{
Expand All @@ -32,7 +36,7 @@ public function decode(string $accessToken): NativeAuthDecoded {

[$origin, $blockHash, $ttl, $extraInfo] = $bodyComponents;

$parsedExtraInfo = $extraInfo === '{}' ? '' : json_decode(base64_decode($this->unescape($extraInfo)));
$parsedExtraInfo = $extraInfo === '{}' ? null : json_decode(base64_decode($this->unescape($extraInfo)), true);
$parsedOrigin = base64_decode($this->unescape($origin));

return new NativeAuthDecoded(
Expand All @@ -49,14 +53,13 @@ public function decode(string $accessToken): NativeAuthDecoded {
public function validate(string $accessToken): NativeAuthValidateResult {
$decoded = $this->decode($accessToken);

throw_unless($decoded->ttl <= $this->maxExpirySeconds, InvalidArgumentException::class, 'token expired');
throw_unless($decoded->ttl <= $this->maxExpirySeconds, NativeAuthInvalidTokenTtlException::class, $decoded->ttl, $this->maxExpirySeconds);

$hasAcceptedOrigins = count($this->acceptedOrigins) > 0;
$isInvalidOrigin = !in_array($decoded->origin, $this->acceptedOrigins) && !in_array('https://' . $decoded->origin, $this->acceptedOrigins);
throw_if($hasAcceptedOrigins && $isInvalidOrigin, InvalidArgumentException::class, "invalid origin: {$decoded->origin}");
throw_if($hasAcceptedOrigins && $isInvalidOrigin, NativeAuthOriginNotAcceptedException::class, $decoded->origin);

// TODO: implement block timestamp & ttl verification:
// https://github.com/multiversx/mx-sdk-js-native-auth-server/blob/5707b04c3d1e40088a1cbe12c3b51fdd6a8ada90/src/native.auth.server.ts#L98
$this->ensureNotExpired($decoded);

$verifiable = new SignableMessage(
message: "{$decoded->address}{$decoded->body}",
Expand All @@ -78,7 +81,7 @@ public function validate(string $accessToken): NativeAuthValidateResult {
->verify($verifiable);
}

throw_unless($valid, InvalidArgumentException::class, 'invalid signature');
throw_unless($valid, NativeAuthInvalidSignatureException::class);

return new NativeAuthValidateResult(
issued: 1, // TODO implement as part of block timestamp & ttl verification
Expand All @@ -89,7 +92,19 @@ public function validate(string $accessToken): NativeAuthValidateResult {
);
}

private function unescape(string $str): string {
private function unescape(string $str): string
{
return str_replace(['-', '_'], ['+', '/'], $str);
}

private function ensureNotExpired(NativeAuthDecoded $decoded): void
{
if (isset($decoded->extraInfo['timestamp'])) {
$timestamp = $decoded->extraInfo['timestamp'];
$expiry = $timestamp + $decoded->ttl;
$now = time();

throw_if($expiry < $now, NativeAuthTokenExpiredException::class);
}
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/NativeAuthInvalidSignatureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Peerme\MxLaravel\Exceptions;

use Exception;

class NativeAuthInvalidSignatureException extends Exception
{
public function __construct()
{
parent::__construct('Invalid signature');
}
}
15 changes: 15 additions & 0 deletions src/Exceptions/NativeAuthInvalidTokenTtlException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Peerme\MxLaravel\Exceptions;

use Exception;

class NativeAuthInvalidTokenTtlException extends Exception
{
public function __construct(int $currentTtl, int $maxTtl)
{
parent::__construct(
"The provided TTL in the token ({$currentTtl}) is larger than the maximum allowed TTL ({$maxTtl})"
);
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/NativeAuthOriginNotAcceptedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Peerme\MxLaravel\Exceptions;

use Exception;

class NativeAuthOriginNotAcceptedException extends Exception
{
public function __construct(string $origin)
{
parent::__construct("Origin ({$origin}) not accepted");
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/NativeAuthTokenExpiredException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Peerme\MxLaravel\Exceptions;

use Exception;

class NativeAuthTokenExpiredException extends Exception
{
public function __construct()
{
parent::__construct('Token expired');
}
}
58 changes: 47 additions & 11 deletions tests/Auth/NativeAuthServerTest.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
<?php

use Peerme\Mx\Address;
use Peerme\Mx\SignableMessage;
use Peerme\Mx\Signature;
use Peerme\Mx\UserSigner;
use Peerme\MxLaravel\Auth\NativeAuthDecoded;
use Peerme\MxLaravel\Auth\NativeAuthServer;
use Peerme\MxLaravel\Exceptions\NativeAuthInvalidSignatureException;
use Peerme\MxLaravel\Exceptions\NativeAuthOriginNotAcceptedException;
use Peerme\MxLaravel\Exceptions\NativeAuthTokenExpiredException;

beforeEach(function () {
$this->address = 'erd1kc7v0lhqu0sclywkgeg4um8ea5nvch9psf2lf8t96j3w622qss8sav2zl8';
$this->signature = '1f384391dd1d17dfb75307fff47bcce05aa1a2a2034089d4ea0c54757895c63520169cc5d6eb4414a1b77abfd185655c13bb5a4233eecf258b64ed05dde36c0d';
$this->blockHash = '591a3cf6fc0d083179f18640e7c63e2b6a0711f95b9d67910bc525139fce106d';
$this->ttl = 86_400;
$this->accessToken = 'ZXJkMWtjN3YwbGhxdTBzY2x5d2tnZWc0dW04ZWE1bnZjaDlwc2YybGY4dDk2ajN3NjIycXNzOHNhdjJ6bDg.ZUdWNFkyaGhibWRsTG1OdmJRLjU5MWEzY2Y2ZmMwZDA4MzE3OWYxODY0MGU3YzYzZTJiNmEwNzExZjk1YjlkNjc5MTBiYzUyNTEzOWZjZTEwNmQuODY0MDAuZXlKMGFXMWxjM1JoYlhBaU9qRTJOemN5T0RBeU16Wjk.1f384391dd1d17dfb75307fff47bcce05aa1a2a2034089d4ea0c54757895c63520169cc5d6eb4414a1b77abfd185655c13bb5a4233eecf258b64ed05dde36c0d';
$alicePem = "-----BEGIN PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th-----
NDEzZjQyNTc1ZjdmMjZmYWQzMzE3YTc3ODc3MTIxMmZkYjgwMjQ1ODUwOTgxZTQ4
YjU4YTRmMjVlMzQ0ZThmOTAxMzk0NzJlZmY2ODg2NzcxYTk4MmYzMDgzZGE1ZDQy
MWYyNGMyOTE4MWU2Mzg4ODIyOGRjODFjYTYwZDY5ZTE=
-----END PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th-----";

$signer = UserSigner::fromPem($alicePem);
$address = 'erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th';
$blockHash = '591a3cf6fc0d083179f18640e7c63e2b6a0711f95b9d67910bc525139fce106d';
$ttl = 86_400;
$origin = 'api.multiversx.com';
$init = rtrim(base64_encode($origin), '=') . '.' . $blockHash . '.' . $ttl . '.' . 'e30';

$signature = $signer->sign((new SignableMessage(
message: $address.$init,
signature: new Signature(''),
address: Address::zero(),
))->serializeForSigning())->hex();

$this->address = $address;
$this->signature = $signature;
$this->blockHash = $blockHash;
$this->ttl = $ttl;
$this->accessToken = rtrim(base64_encode($this->address), '=') . '.' . rtrim(base64_encode($init), '=') . '.' . $signature;
$this->blockTimestamp = 1671009408;
$this->origin = 'xexchange.com';
$this->origin = $origin;
$this->nativeServerConfig = [
'acceptedOrigins' => ['https://xexchange.com'],
'acceptedOrigins' => [$origin],
'maxExpirySeconds' => 86_400,
'apiUrl' => 'https://api.multiversx.com',
];
Expand All @@ -39,19 +65,29 @@
expect($actual->address)->toBe($this->address);
});

it('throws if invalid signature in access token', function () {
it('throws when invalid signature in access token', function () {
$subject = new NativeAuthServer(...$this->nativeServerConfig);

$subject->validate($this->accessToken.'abcdef');
})
->expectExceptionMessage('invalid signature');
->expectException(NativeAuthInvalidSignatureException::class);

it('throws if invalid origin', function () {
it('throws when invalid origin', function () {
$subject = new NativeAuthServer(...[
...$this->nativeServerConfig,
'acceptedOrigins' => ['other-origin'],
]);

$subject->validate($this->accessToken);
})
->expectExceptionMessage('invalid origin');
->expectException(NativeAuthOriginNotAcceptedException::class);

it('throws when extra info timestamp exceeds ttl', function () {
$subject = new NativeAuthServer(...[
...$this->nativeServerConfig,
'acceptedOrigins' => ['localhost'],
]);

$subject->validate('ZXJkMXdqeXRmbjZ6aHFmY3NlanZod3Y3cTR1c2F6czVyeWMzajhoYzc4ZmxkZ2pueWN0OHdlanFrYXN1bmM.Ykc5allXeG9iM04wLjE4YmM5ODI0NjFkMWI1M2M4MzdhMjRkZTRiNDYyM2MyYmI4MzU4NjdlYTJlOGRmMTQzNjVjZjQzNmRlZTFiMjMuNjAwLmV5SjBhVzFsYzNSaGJYQWlPakUyTnpNNU56SXpOalI5.f8d651eda06e82a894ff1dc9480a33aa1030b076dfd5983346eec6793381587b88c2daf770a10ac39f9911968c2f1d1304c0c7dd86a82bc79f07e89f873f7e02');
})
->expectException(NativeAuthTokenExpiredException::class);

0 comments on commit 801d104

Please sign in to comment.