diff --git a/src/Auth/NativeAuthServer.php b/src/Auth/NativeAuthServer.php index caad3f8..ff9d220 100644 --- a/src/Auth/NativeAuthServer.php +++ b/src/Auth/NativeAuthServer.php @@ -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 { @@ -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( @@ -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}", @@ -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 @@ -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); + } + } } diff --git a/src/Exceptions/NativeAuthInvalidSignatureException.php b/src/Exceptions/NativeAuthInvalidSignatureException.php new file mode 100644 index 0000000..5b9e48d --- /dev/null +++ b/src/Exceptions/NativeAuthInvalidSignatureException.php @@ -0,0 +1,13 @@ +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', ]; @@ -39,14 +65,14 @@ 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'], @@ -54,4 +80,14 @@ $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);