├── README.md ├── .gitignore ├── resources └── config │ └── shield.php ├── src ├── Exceptions │ ├── Exception.php │ ├── UnknownServiceException.php │ └── UnsupportedDriverException.php ├── Contracts │ └── Service.php ├── Support │ └── BasicAuth.php ├── Providers │ └── ShieldServiceProvider.php ├── Http │ └── Middleware │ │ └── Shield.php └── Manager.php ├── .travis.yml ├── .scrutinizer.yml ├── composer.json ├── LICENSE ├── phpunit.xml └── tests └── Unit ├── Support └── BasicAuthTest.php ├── MiddlewareTest.php └── ManagerTest.php /README.md: -------------------------------------------------------------------------------- 1 | # Shield 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | .DS_Store 4 | coverage.xml 5 | composer.lock 6 | -------------------------------------------------------------------------------- /resources/config/shield.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | ] 8 | 9 | ]; 10 | -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | hasHeader('PHP-AUTH-USER') && $request->hasHeader('PHP-AUTH-PW')) { 19 | return $request->header('PHP-AUTH-USER') == $username && $request->header('PHP-AUTH-PW') == $password; 20 | } 21 | 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Providers/ShieldServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__ . '/../../resources/config/shield.php' => config_path('shield.php'), 19 | ], 'config'); 20 | 21 | $this->app['router']->aliasMiddleware('shield', Shield::class); 22 | } 23 | 24 | public function register() 25 | { 26 | $this->mergeConfigFrom( 27 | __DIR__ . '/../../resources/config/shield.php', 28 | 'shield' 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | excluded_paths: [tests/*] 4 | checks: 5 | php: 6 | code_rating: true 7 | remove_extra_empty_lines: true 8 | remove_php_closing_tag: true 9 | remove_trailing_whitespace: true 10 | fix_use_statements: 11 | remove_unused: true 12 | preserve_multiple: false 13 | preserve_blanklines: true 14 | order_alphabetically: true 15 | fix_php_opening_tag: true 16 | fix_linefeed: true 17 | fix_line_ending: true 18 | fix_identation_4spaces: true 19 | fix_doc_comments: true 20 | tools: 21 | external_code_coverage: true 22 | php_code_coverage: false 23 | php_code_sniffer: 24 | config: 25 | standard: PSR2 26 | filter: 27 | paths: ['src'] 28 | php_loc: 29 | enabled: true 30 | excluded_dirs: [vendor] 31 | php_cpd: 32 | enabled: true 33 | excluded_dirs: [vendor] 34 | -------------------------------------------------------------------------------- /src/Http/Middleware/Shield.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 20 | } 21 | 22 | /** 23 | * Handle an incoming request. 24 | * 25 | * @param \Illuminate\Http\Request $request 26 | * @param \Closure $next 27 | * @param string $service 28 | * 29 | * @return mixed 30 | */ 31 | public function handle(Request $request, Closure $next, string $service) 32 | { 33 | if($this->manager->passes($service, $request)) { 34 | return $next($request); 35 | } 36 | 37 | return Response::create(Response::$statusTexts[Response::HTTP_BAD_REQUEST], Response::HTTP_BAD_REQUEST); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-shield/shield", 3 | "description": "A laravel middleware to shield against unverified webhooks from 3rd party services.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Ashley Clarke", 9 | "email": "me@ashleyclarke.me" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.0.0", 14 | "laravel/framework": "^5.5" 15 | }, 16 | "require-dev": { 17 | "laravel-shield/testing": "~1.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Shield\\Shield\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Shield\\Shield\\Test\\": "tests/" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Shield\\Shield\\Providers\\ShieldServiceProvider" 33 | ] 34 | } 35 | }, 36 | "config": { 37 | "preferred-install": "dist", 38 | "platform": { 39 | "php": "7.0" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Laravel Shield 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | ./tests/Unit 18 | 19 | 20 | 21 | 22 | 23 | ./src 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | validate($service); 18 | 19 | if ($this->checkHeaders($instance->headers(), $request)) { 20 | return $instance->verify($request, collect(config('shield.services.' . $service . '.options', []))); 21 | } 22 | 23 | return false; 24 | } 25 | 26 | /** 27 | * @param string $service 28 | * 29 | * @return Service 30 | */ 31 | protected function validate(string $service) 32 | { 33 | throw_unless(Arr::exists(config('shield.services'), $service), UnknownServiceException::class, sprintf('Service [%s] not found.', $service)); 34 | throw_unless(Arr::exists(config('shield.services.' . $service), 'driver'), UnknownServiceException::class, sprintf('Service [%s] must have a driver.', $service)); 35 | 36 | $service = app(config('shield.services.' . $service . '.driver')); 37 | 38 | throw_unless($service instanceof Service, UnsupportedDriverException::class, sprintf('Driver [%s] must implement [%s].', get_class($service), Service::class)); 39 | 40 | return $service; 41 | } 42 | 43 | public function checkHeaders(array $headers, Request $request) 44 | { 45 | foreach ($headers as $header) { 46 | if (!$request->hasHeader($header)) return false; 47 | } 48 | 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/Support/BasicAuthTest.php: -------------------------------------------------------------------------------- 1 | service = new Example; 29 | } 30 | 31 | /** @test */ 32 | public function it_will_fail_if_invalid_credentials() 33 | { 34 | $request = $this->request(); 35 | $request->headers->add([ 36 | 'PHP-AUTH-USER' => 'user', 37 | 'PHP-AUTH-PW' => 'password', 38 | ]); 39 | 40 | Assert::assertFalse($this->service->checkBasic($request, 'user', 'pass')); 41 | } 42 | 43 | /** @test */ 44 | public function it_will_fail_if_invalid_headers() 45 | { 46 | $request = $this->request(); 47 | $request->headers->add([ 48 | 'PHP-AUTH-USER' => 'user', 49 | 'PHP-AUTH-PASS' => 'pass', 50 | ]); 51 | 52 | Assert::assertFalse($this->service->checkBasic($request, 'user', 'pass')); 53 | } 54 | 55 | /** @test */ 56 | public function it_will_pass_if_correct_credentials() 57 | { 58 | $request = $this->request(); 59 | $request->headers->add([ 60 | 'PHP-AUTH-USER' => 'user', 61 | 'PHP-AUTH-PW' => 'pass', 62 | ]); 63 | 64 | Assert::assertTrue($this->service->checkBasic($request, 'user', 'pass')); 65 | } 66 | } 67 | 68 | class Example implements Service { 69 | 70 | use BasicAuth; 71 | 72 | public function verify(Request $request, Collection $config): bool 73 | { 74 | return true; 75 | } 76 | 77 | public function headers(): array 78 | { 79 | return []; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Unit/MiddlewareTest.php: -------------------------------------------------------------------------------- 1 | app['config']['shield.services.example'] = [ 25 | 'driver' => Bad::class 26 | ]; 27 | 28 | $manager = new Manager; 29 | $middleware = new Shield($manager); 30 | 31 | /** @var Response $response */ 32 | $response = $middleware->handle($this->request(), function (){}, 'example'); 33 | 34 | Assert::assertInstanceOf(Response::class, $response); 35 | Assert::assertEquals(400, $response->getStatusCode()); 36 | Assert::assertEquals('Bad Request', $response->getContent()); 37 | } 38 | 39 | /** @test */ 40 | public function it_calls_next_if_successful() 41 | { 42 | $this->app['config']['shield.services.example'] = [ 43 | 'driver' => Good::class 44 | ]; 45 | 46 | $manager = new Manager; 47 | $middleware = new Shield($manager); 48 | 49 | $resp = Response::create('Test', 200); 50 | 51 | $closure = function () use ($resp) { 52 | return $resp; 53 | }; 54 | 55 | /** @var Response $response */ 56 | $response = $middleware->handle($this->request(), $closure, 'example'); 57 | 58 | Assert::assertSame($resp, $response); 59 | } 60 | } 61 | 62 | class Good implements Service { 63 | 64 | public function verify(Request $request, Collection $config): bool 65 | { 66 | return true; 67 | } 68 | 69 | public function headers(): array 70 | { 71 | return []; 72 | } 73 | } 74 | 75 | class Bad implements Service { 76 | 77 | public function verify(Request $request, Collection $config): bool 78 | { 79 | return false; 80 | } 81 | 82 | public function headers(): array 83 | { 84 | return []; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Unit/ManagerTest.php: -------------------------------------------------------------------------------- 1 | manager = new Manager; 29 | } 30 | 31 | /** 32 | * @test 33 | * @expectedException \Shield\Shield\Exceptions\UnknownServiceException 34 | * @expectedExceptionMessage Service [unknown] not found. 35 | */ 36 | public function it_throws_exception_if_service_not_found() 37 | { 38 | $this->manager->passes('unknown', $this->request()); 39 | } 40 | 41 | /** 42 | * @test 43 | * @expectedException \Shield\Shield\Exceptions\UnknownServiceException 44 | * @expectedExceptionMessage Service [unknown] must have a driver. 45 | */ 46 | public function it_throws_exception_if_service_does_not_have_a_driver() 47 | { 48 | $this->app['config']['shield.services.unknown'] = []; 49 | 50 | $this->manager->passes('unknown', $this->request()); 51 | } 52 | 53 | /** 54 | * @test 55 | * @expectedException \Shield\Shield\Exceptions\UnsupportedDriverException 56 | * @expectedExceptionMessage Driver [Shield\Shield\Test\Random] must implement [Shield\Shield\Contracts\Service]. 57 | */ 58 | public function it_throws_exception_if_driver_is_not_a_service() 59 | { 60 | $this->app['config']['shield.services.example'] = [ 61 | 'driver' => Random::class 62 | ]; 63 | 64 | $this->manager->passes('example', $this->request()); 65 | } 66 | 67 | /** @test */ 68 | public function it_fails_if_expected_header_is_not_there() 69 | { 70 | $this->app['config']['shield.services.example'] = [ 71 | 'driver' => Example::class 72 | ]; 73 | 74 | Assert::assertFalse($this->manager->passes('example', $this->request())); 75 | } 76 | 77 | /** @test */ 78 | public function it_passes_if_expected_header_is_there() 79 | { 80 | $this->app['config']['shield.services.example'] = [ 81 | 'driver' => Example::class 82 | ]; 83 | 84 | $request = $this->request(); 85 | $request->headers->add(['X-Custom-Header' => 'custom data']); 86 | 87 | Assert::assertTrue($this->manager->passes('example', $request)); 88 | } 89 | } 90 | 91 | class Example implements Service { 92 | 93 | public function verify(Request $request, Collection $config): bool 94 | { 95 | return true; 96 | } 97 | 98 | public function headers(): array 99 | { 100 | return ['X-Custom-Header']; 101 | } 102 | } 103 | 104 | class Random {} 105 | --------------------------------------------------------------------------------