From a1d6371d7c641ce4bf4b13d74c5f46114435c058 Mon Sep 17 00:00:00 2001 From: Darryn Ten Date: Thu, 15 Oct 2020 19:16:37 +0700 Subject: [PATCH] JWT Support (#601) * Work in Progress * Add new config option * Work in progress * Shopify token middleware * Remove redundant command * Removed debugging code * Revert some changes back to master versions * No longer need the JWT env option * Reverted newlines * Removed custom package * Expiration bugfix * Add root api route * Split routes * Authentication token tests * Nove logic * Update unit tests * Update unit tests * Reverted composer * Removed * Lint fix * Drop php 7.2 * Only use legacy factories when Laravel 8 is in use * Remove legacy package * Add docblock for helpers * Missing docblock * Style CI fixes * Style CI fixes * Force the middleware * Updated to throw exceptions instead of a plain response on token errors * Exception handler for bad tokens * Style CI fixes * trigger GitHub actions * Make sure the expiration in the test tokens is in the future --- .github/workflows/ci.yml | 4 +- phpunit.xml.dist | 2 + src/ShopifyApp/Actions/AuthorizeShop.php | 43 +- src/ShopifyApp/Exceptions/HttpException.php | 20 + .../Http/Controllers/ApiController.php | 24 + src/ShopifyApp/Http/Middleware/AuthToken.php | 129 +++++ src/ShopifyApp/Http/Middleware/Billable.php | 2 +- src/ShopifyApp/Services/ApiHelper.php | 1 + src/ShopifyApp/ShopifyAppProvider.php | 5 +- src/ShopifyApp/Traits/ApiController.php | 48 ++ src/ShopifyApp/helpers.php | 30 + src/ShopifyApp/resources/routes/api.php | 61 +++ .../{routes.php => routes/shopify.php} | 21 +- tests/Actions/AuthenticateShopTest.php | 2 +- tests/Actions/AuthorizeShopTest.php | 31 +- tests/Http/Middleware/AuthShopifyTest.php | 6 +- tests/Http/Middleware/AuthTokenTest.php | 490 +++++++++++++++++ tests/Http/Middleware/AuthWebhookTest.php | 2 +- tests/Services/ApiHelperTest.php | 3 +- tests/Stubs/Kernel.php | 1 + tests/Traits/ApiControllerTest.php | 511 ++++++++++++++++++ tests/Traits/AuthControllerTest.php | 8 +- tests/Traits/HomeControllerTest.php | 2 +- tests/Traits/WebhookControllerTest.php | 2 +- 24 files changed, 1392 insertions(+), 56 deletions(-) create mode 100644 src/ShopifyApp/Exceptions/HttpException.php create mode 100644 src/ShopifyApp/Http/Controllers/ApiController.php create mode 100644 src/ShopifyApp/Http/Middleware/AuthToken.php create mode 100644 src/ShopifyApp/Traits/ApiController.php create mode 100644 src/ShopifyApp/resources/routes/api.php rename src/ShopifyApp/resources/{routes.php => routes/shopify.php} (87%) create mode 100644 tests/Http/Middleware/AuthTokenTest.php create mode 100644 tests/Traits/ApiControllerTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 938f216b..ef029ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: run: composer validate --strict - name: Install Laravel legacy factories support - if: matrix.php > '7.2' && matrix.laravel == '8.0' + if: matrix.laravel == '8.0' run: composer require "laravel/legacy-factories:^1.0" --no-interaction --no-update - name: Install Laravel and Orchestra Testbench @@ -62,7 +62,7 @@ jobs: run: vendor/bin/phpunit - name: Upload coverage results - if: matrix.php == '7.4' && matrix.laravel == '7.0' + if: matrix.php == '7.4' && matrix.laravel == '8.0' env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 19c08326..87ae076f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -41,5 +41,7 @@ + + diff --git a/src/ShopifyApp/Actions/AuthorizeShop.php b/src/ShopifyApp/Actions/AuthorizeShop.php index 1a0be3ac..cb5fcd13 100644 --- a/src/ShopifyApp/Actions/AuthorizeShop.php +++ b/src/ShopifyApp/Actions/AuthorizeShop.php @@ -75,7 +75,6 @@ public function __invoke(ShopDomain $shopDomain, ?string $code): stdClass $this->shopCommand->make($shopDomain, NullAccessToken::fromNative(null)); $shop = $this->shopQuery->getByDomain($shopDomain); } - $apiHelper = $shop->apiHelper(); // Return data $return = [ @@ -83,26 +82,34 @@ public function __invoke(ShopDomain $shopDomain, ?string $code): stdClass 'url' => null, ]; - // Start the process + $apiHelper = $shop->apiHelper(); + + // Access/grant mode + $grantMode = $shop->hasOfflineAccess() ? + AuthMode::fromNative($this->getConfig('api_grant_mode')) : + AuthMode::OFFLINE(); + + $return['url'] = $apiHelper->buildAuthUrl($grantMode, $this->getConfig('api_scopes')); + + // If there's no code if (empty($code)) { - // Access/grant mode - $grantMode = $shop->hasOfflineAccess() ? - AuthMode::fromNative($this->getConfig('api_grant_mode')) : - AuthMode::OFFLINE(); - - // Call the partial callback with the shop and auth URL as params - $return['url'] = $apiHelper->buildAuthUrl($grantMode, $this->getConfig('api_scopes')); - } else { - // if the store has been deleted, restore the store to set the access token - if ($shop->trashed()) { - $shop->restore(); - } - - // We have a good code, get the access details - $this->shopSession->make($shop->getDomain()); - $this->shopSession->setAccess($apiHelper->getAccessData($code)); + return (object) $return; + } + + // if the store has been deleted, restore the store to set the access token + if ($shop->trashed()) { + $shop->restore(); + } + // We have a good code, get the access details + $this->shopSession->make($shop->getDomain()); + + try { + $this->shopSession->setAccess($apiHelper->getAccessData($code)); + $return['url'] = null; $return['completed'] = true; + } catch (\Exception $e) { + // Just return the default setting } return (object) $return; diff --git a/src/ShopifyApp/Exceptions/HttpException.php b/src/ShopifyApp/Exceptions/HttpException.php new file mode 100644 index 00000000..8f34eb35 --- /dev/null +++ b/src/ShopifyApp/Exceptions/HttpException.php @@ -0,0 +1,20 @@ +expectsJson()) { + return response()->json([ + 'error' => $this->getMessage(), + ], $this->getCode()); + } + + return response($this->getMessage(), $this->getCode()); + } +} diff --git a/src/ShopifyApp/Http/Controllers/ApiController.php b/src/ShopifyApp/Http/Controllers/ApiController.php new file mode 100644 index 00000000..fbb44855 --- /dev/null +++ b/src/ShopifyApp/Http/Controllers/ApiController.php @@ -0,0 +1,24 @@ +middleware('auth.token'); + } +} diff --git a/src/ShopifyApp/Http/Middleware/AuthToken.php b/src/ShopifyApp/Http/Middleware/AuthToken.php new file mode 100644 index 00000000..f70f5e45 --- /dev/null +++ b/src/ShopifyApp/Http/Middleware/AuthToken.php @@ -0,0 +1,129 @@ +shopSession = $shopSession; + } + + /** + * Handle an incoming request. + * + * Get the bearer token, validate and verify, and create a + * session based on the contents. + * + * The token is "url safe" (`+` is `-` and `/` is `_`) base64. + * + * @param Request $request The request object. + * @param \Closure $next The next action. + * + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + $now = time(); + + $token = $request->bearerToken(); + + if (! $token) { + throw new HttpException('Missing authentication token', 401); + } + + // The header is fixed so include it here + if (! preg_match('/^eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9\-\_=]+\.[A-Za-z0-9\-\_\=]*$/', $token)) { + throw new HttpException('Malformed token', 400); + } + + if (! $this->checkSignature($token)) { + throw new HttpException('Unable to verify signature', 400); + } + + $parts = explode('.', $token); + + $body = base64url_decode($parts[1]); + $signature = $parts[2]; + + $body = json_decode($body); + + if (! $body || + ! isset($body->iss) || + ! isset($body->dest) || + ! isset($body->aud) || + ! isset($body->sub) || + ! isset($body->exp) || + ! isset($body->nbf) || + ! isset($body->iat) || + ! isset($body->jti) || + ! isset($body->sid)) { + throw new HttpException('Malformed token', 400); + } + + if (($now > $body->exp) || ($now < $body->nbf) || ($now < $body->iat)) { + throw new HttpException('Expired token', 403); + } + + if (! stristr($body->iss, $body->dest)) { + throw new HttpException('Invalid token', 400); + } + + if ($body->aud !== $this->getConfig('api_key')) { + throw new HttpException('Invalid token', 400); + } + + // All is well, login + $url = parse_url($body->dest); + + $this->shopSession->make(ShopDomain::fromNative($url['host'])); + $this->shopSession->setSessionToken($body->sid); + + return $next($request); + } + + /** + * Checks the validity of the signature sent with the token. + * + * @param string $token The token to check. + * + * @return bool + */ + private function checkSignature($token) + { + $parts = explode('.', $token); + $signature = array_pop($parts); + $check = implode('.', $parts); + + $secret = $this->getConfig('api_secret'); + $hmac = hash_hmac('sha256', $check, $secret, true); + $encoded = base64url_encode($hmac); + + return $encoded === $signature; + } +} diff --git a/src/ShopifyApp/Http/Middleware/Billable.php b/src/ShopifyApp/Http/Middleware/Billable.php index 651640af..1e3d2b7b 100644 --- a/src/ShopifyApp/Http/Middleware/Billable.php +++ b/src/ShopifyApp/Http/Middleware/Billable.php @@ -48,7 +48,7 @@ public function handle(Request $request, Closure $next) $shop = $this->shopSession->getShop(); if (! $shop->isFreemium() && ! $shop->isGrandfathered() && ! $shop->plan) { // They're not grandfathered in, and there is no charge or charge was declined... redirect to billing - return Redirect::route('billing'); + return Redirect::route('billing', $request->input()); } } diff --git a/src/ShopifyApp/Services/ApiHelper.php b/src/ShopifyApp/Services/ApiHelper.php index 9b190a44..59184376 100644 --- a/src/ShopifyApp/Services/ApiHelper.php +++ b/src/ShopifyApp/Services/ApiHelper.php @@ -41,6 +41,7 @@ public function make(Session $session = null): self { // Create the options $opts = new Options(); + $opts->setApiKey($this->getConfig('api_key')); $opts->setApiSecret($this->getConfig('api_secret')); $opts->setVersion($this->getConfig('api_version')); diff --git a/src/ShopifyApp/ShopifyAppProvider.php b/src/ShopifyApp/ShopifyAppProvider.php index 60e47a62..5eed1689 100644 --- a/src/ShopifyApp/ShopifyAppProvider.php +++ b/src/ShopifyApp/ShopifyAppProvider.php @@ -26,6 +26,7 @@ use Osiset\ShopifyApp\Contracts\Queries\Shop as IShopQuery; use Osiset\ShopifyApp\Http\Middleware\AuthProxy; use Osiset\ShopifyApp\Http\Middleware\AuthShopify; +use Osiset\ShopifyApp\Http\Middleware\AuthToken; use Osiset\ShopifyApp\Http\Middleware\AuthWebhook; use Osiset\ShopifyApp\Http\Middleware\Billable; use Osiset\ShopifyApp\Messaging\Jobs\ScripttagInstaller; @@ -255,7 +256,8 @@ public function register() */ private function bootRoutes(): void { - $this->loadRoutesFrom(__DIR__.'/resources/routes.php'); + $this->loadRoutesFrom(__DIR__.'/resources/routes/shopify.php'); + $this->loadRoutesFrom(__DIR__.'/resources/routes/api.php'); } /** @@ -352,6 +354,7 @@ private function bootMiddlewares(): void { // Middlewares $this->app['router']->aliasMiddleware('auth.shopify', AuthShopify::class); + $this->app['router']->aliasMiddleware('auth.token', AuthToken::class); $this->app['router']->aliasMiddleware('auth.webhook', AuthWebhook::class); $this->app['router']->aliasMiddleware('auth.proxy', AuthProxy::class); $this->app['router']->aliasMiddleware('billable', Billable::class); diff --git a/src/ShopifyApp/Traits/ApiController.php b/src/ShopifyApp/Traits/ApiController.php new file mode 100644 index 00000000..23c860aa --- /dev/null +++ b/src/ShopifyApp/Traits/ApiController.php @@ -0,0 +1,48 @@ +json(); + } + + /** + * Returns authenticated users details. + * + * @return JsonResponse + */ + public function getSelf(): JsonResponse + { + return response()->json(Auth::user()->only([ + 'name', + 'shopify_grandfathered', + 'shopify_freemium', + 'plan', + ])); + } + + /** + * Returns currently available plans. + * + * @return JsonResponse + */ + public function getPlans(): JsonResponse + { + return response()->json(Plan::all()); + } +} diff --git a/src/ShopifyApp/helpers.php b/src/ShopifyApp/helpers.php index 2247ed78..dae48aec 100644 --- a/src/ShopifyApp/helpers.php +++ b/src/ShopifyApp/helpers.php @@ -85,6 +85,36 @@ function parseQueryString(string $qs, string $d = null): array return $params; } +/** + * URL-safe Base64 encoding. + * + * Replaces `+` with `-` and `/` with `_` and trims padding `=`. + * + * @param string $data The data to be encoded. + * + * @return string + */ +function base64url_encode($data) +{ + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); +} + +/** + * URL-safe Base64 decoding. + * + * Replaces `-` with `+` and `_` with `/`. + * + * Adds padding `=` if needed. + * + * @param string $data The data to be decoded. + * + * @return string + */ +function base64url_decode($data) +{ + return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT)); +} + /** * Checks if the route should be registered or not. * diff --git a/src/ShopifyApp/resources/routes/api.php b/src/ShopifyApp/resources/routes/api.php new file mode 100644 index 00000000..d558db8b --- /dev/null +++ b/src/ShopifyApp/resources/routes/api.php @@ -0,0 +1,61 @@ + ['api']], function ($manualRoutes) { + /* + |-------------------------------------------------------------------------- + | API Routes + |-------------------------------------------------------------------------- + | + | Exposes endpoints for the current user data, and all plans. + | + */ + + if (registerPackageRoute('api', $manualRoutes)) { + Route::group(['prefix' => 'api', 'middleware' => ['auth.token']], function () { + Route::get( + '/', + 'Osiset\ShopifyApp\Http\Controllers\ApiController@index' + ); + + Route::get( + '/me', + 'Osiset\ShopifyApp\Http\Controllers\ApiController@getSelf' + ); + + Route::get( + '/plans', + 'Osiset\ShopifyApp\Http\Controllers\ApiController@getPlans' + ); + }); + } + + /* + |-------------------------------------------------------------------------- + | Webhook Handler + |-------------------------------------------------------------------------- + | + | Handles incoming webhooks. + | + */ + + if (registerPackageRoute('webhook', $manualRoutes)) { + Route::post( + '/webhook/{type}', + 'Osiset\ShopifyApp\Http\Controllers\WebhookController@handle' + ) + ->middleware('auth.webhook') + ->name('webhook'); + } +}); diff --git a/src/ShopifyApp/resources/routes.php b/src/ShopifyApp/resources/routes/shopify.php similarity index 87% rename from src/ShopifyApp/resources/routes.php rename to src/ShopifyApp/resources/routes/shopify.php index a7575b18..e13b5bf9 100644 --- a/src/ShopifyApp/resources/routes.php +++ b/src/ShopifyApp/resources/routes/shopify.php @@ -15,6 +15,7 @@ // Check if manual routes override is to be use $manualRoutes = Config::get('shopify-app.manual_routes'); + if ($manualRoutes) { // Get a list of route names to exclude $manualRoutes = explode(',', $manualRoutes); @@ -132,23 +133,3 @@ ->name('billing.usage_charge'); } }); - -Route::group(['middleware' => ['api']], function () use ($manualRoutes) { - /* - |-------------------------------------------------------------------------- - | Webhook Handler - |-------------------------------------------------------------------------- - | - | Handles incoming webhooks. - | - */ - - if (registerPackageRoute('webhook', $manualRoutes)) { - Route::post( - '/webhook/{type}', - 'Osiset\ShopifyApp\Http\Controllers\WebhookController@handle' - ) - ->middleware('auth.webhook') - ->name('webhook'); - } -}); diff --git a/tests/Actions/AuthenticateShopTest.php b/tests/Actions/AuthenticateShopTest.php index 00c32ab4..fec9f0e6 100644 --- a/tests/Actions/AuthenticateShopTest.php +++ b/tests/Actions/AuthenticateShopTest.php @@ -94,7 +94,7 @@ public function testRuns(): void // Query Params [ 'shop' => 'mystore123.myshopify.com', - 'hmac' => '9f4d79eb5ab1806c390b3dda0bfc7be714a92df165d878f22cf3cc8145249ca8', + 'hmac' => '3d9768c9cc44b8bd66125cb82b6a59a3d835432f560d19b3f79b9fc696ef6396', 'timestamp' => '1565631587', 'code' => '123', 'locale' => 'de', diff --git a/tests/Actions/AuthorizeShopTest.php b/tests/Actions/AuthorizeShopTest.php index 99546881..ea18d02e 100644 --- a/tests/Actions/AuthorizeShopTest.php +++ b/tests/Actions/AuthorizeShopTest.php @@ -30,7 +30,7 @@ public function testNoShopShouldBeMade(): void ); $this->assertStringContainsString( - '/admin/oauth/authorize?client_id=&scope=read_products%2Cwrite_products&redirect_uri=https%3A%2F%2Flocalhost%2Fauthenticate', + '/admin/oauth/authorize?client_id='.env('SHOPIFY_API_KEY').'&scope=read_products%2Cwrite_products&redirect_uri=https%3A%2F%2Flocalhost%2Fauthenticate', $result->url ); $this->assertFalse($result->completed); @@ -48,7 +48,7 @@ public function testWithoutCode(): void ); $this->assertStringContainsString( - '/admin/oauth/authorize?client_id=&scope=read_products%2Cwrite_products&redirect_uri=https%3A%2F%2Flocalhost%2Fauthenticate', + '/admin/oauth/authorize?client_id='.env('SHOPIFY_API_KEY').'&scope=read_products%2Cwrite_products&redirect_uri=https%3A%2F%2Flocalhost%2Fauthenticate', $result->url ); $this->assertFalse($result->completed); @@ -78,4 +78,31 @@ public function testWithCode(): void $this->assertTrue($result->completed); $this->assertNotSame($currentToken->toNative(), $shop->getToken()->toNative()); } + + public function testWithCodeSoftDeletedShop(): void + { + // Create the shop + $shop = factory($this->model)->create([ + 'deleted_at' => time(), + ]); + + // Get the current access token + $currentToken = $shop->getToken(); + + // Setup API stub + $this->setApiStub(); + ApiStub::stubResponses(['access_token']); + + $result = call_user_func( + $this->action, + $shop->getDomain(), + '12345678' + ); + + // Refresh to see changes + $shop->refresh(); + + $this->assertTrue($result->completed); + $this->assertNotSame($currentToken->toNative(), $shop->getToken()->toNative()); + } } diff --git a/tests/Http/Middleware/AuthShopifyTest.php b/tests/Http/Middleware/AuthShopifyTest.php index 1a6bea65..16f6a081 100644 --- a/tests/Http/Middleware/AuthShopifyTest.php +++ b/tests/Http/Middleware/AuthShopifyTest.php @@ -35,7 +35,7 @@ public function testQueryInput(): void // Query Params [ 'shop' => 'mystore123.myshopify.com', - 'hmac' => '9f4d79eb5ab1806c390b3dda0bfc7be714a92df165d878f22cf3cc8145249ca8', + 'hmac' => '3d9768c9cc44b8bd66125cb82b6a59a3d835432f560d19b3f79b9fc696ef6396', 'timestamp' => '1565631587', 'code' => '123', 'locale' => 'de', @@ -119,7 +119,7 @@ public function testReferer(): void null, // Server vars array_merge(Request::server(), [ - 'HTTP_REFERER' => 'https://xxx.com?shop=example.myshopify.com&hmac=a7448f7c42c9bc025b077ac8b73e7600b6f8012719d21cbeb88db66e5dbbd163×tamp=1337178173&code=1234678', + 'HTTP_REFERER' => 'https://xxx.com?shop=example.myshopify.com&hmac=6f16da24e8185e717f22a3373a1928fcaea7ea2401be40ab0d160f5bed7fe55a×tamp=1337178173&code=1234678', ]) ); @@ -155,7 +155,7 @@ public function testHeaders(): void ); $newRequest->headers->set('X-Shop-Domain', 'example.myshopify.com'); - $newRequest->headers->set('X-Shop-Signature', 'a7448f7c42c9bc025b077ac8b73e7600b6f8012719d21cbeb88db66e5dbbd163'); + $newRequest->headers->set('X-Shop-Signature', '6f16da24e8185e717f22a3373a1928fcaea7ea2401be40ab0d160f5bed7fe55a'); $newRequest->headers->set('X-Shop-Time', '1337178173'); $newRequest->headers->set('X-Shop-Code', '1234678'); diff --git a/tests/Http/Middleware/AuthTokenTest.php b/tests/Http/Middleware/AuthTokenTest.php new file mode 100644 index 00000000..e00dc496 --- /dev/null +++ b/tests/Http/Middleware/AuthTokenTest.php @@ -0,0 +1,490 @@ +duplicate( + // Query Params + [], + // Request Params + null, + // Attributes + null, + // Cookies + null, + // Files + null, + // Server vars + null + ); + Request::swap($newRequest); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Missing authentication token'); + $this->expectExceptionCode(401); + + // Run the middleware + $response = ($this->app->make(AuthTokenMiddleware::class))->handle(request(), function ($r) { + // ... + }); + } + + public function testDenysForBearerNoJwt(): void + { + $currentRequest = Request::instance(); + $newRequest = $currentRequest->duplicate( + // Query Params + [], + // Request Params + null, + // Attributes + null, + // Cookies + null, + // Files + null, + // Server vars + // This valid referer should be ignored as there is a get variable + array_merge(Request::server(), [ + 'HTTP_AUTHORIZATION' => 'Bearer', + ]) + ); + Request::swap($newRequest); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Missing authentication token'); + $this->expectExceptionCode(401); + + // Run the middleware + $response = ($this->app->make(AuthTokenMiddleware::class))->handle(request(), function () { + // ... + }); + } + + public function testDenysForInvalidJwt(): void + { + $currentRequest = Request::instance(); + $newRequest = $currentRequest->duplicate( + // Query Params + [], + // Request Params + null, + // Attributes + null, + // Cookies + null, + // Files + null, + // Server vars + // This valid referer should be ignored as there is a get variable + array_merge(Request::server(), [ + 'HTTP_AUTHORIZATION' => 'Bearer 1234', + ]) + ); + Request::swap($newRequest); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Malformed token'); + $this->expectExceptionCode(400); + + // Run the middleware + $response = ($this->app->make(AuthTokenMiddleware::class))->handle(request(), function () { + // ... + }); + } + + public function testDenysForValidRegexBadContent(): void + { + $currentRequest = Request::instance(); + $newRequest = $currentRequest->duplicate( + // Query Params + [], + // Request Params + null, + // Attributes + null, + // Cookies + null, + // Files + null, + // Server vars + // This valid referer should be ignored as there is a get variable + array_merge(Request::server(), [ + 'HTTP_AUTHORIZATION' => 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.AAAA.AAAA', + ]) + ); + Request::swap($newRequest); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Unable to verify signature'); + $this->expectExceptionCode(400); + + // Run the middleware + $response = ($this->app->make(AuthTokenMiddleware::class))->handle(request(), function () { + // ... + }); + } + + public function testDenysForValidRegexMissingContent(): void + { + $currentRequest = Request::instance(); + $newRequest = $currentRequest->duplicate( + // Query Params + [], + // Request Params + null, + // Attributes + null, + // Cookies + null, + // Files + null, + // Server vars + // This valid referer should be ignored as there is a get variable + array_merge(Request::server(), [ + 'HTTP_AUTHORIZATION' => 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..', + ]) + ); + Request::swap($newRequest); + + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Malformed token'); + $this->expectExceptionCode(400); + + // Run the middleware + $response = ($this->app->make(AuthTokenMiddleware::class))->handle(request(), function () { + // ... + }); + } + + public function testDenysForValidRegexValidSignatureBadBody(): void + { + $invalidBody = base64url_encode(json_encode([ + 'dest' => '', + 'aud' => '', + 'sub' => '', + 'exp' => '