From 9ff43f641bba2cbc80a1bc63fb9d9bc3ab4b3838 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Tue, 20 Sep 2022 21:15:44 +0300 Subject: [PATCH 1/4] simpler api for default security scheme --- docs/usage/access.md | 6 +++--- src/Support/Generator/OpenApi.php | 10 +++++++++ src/Support/Generator/SecurityScheme.php | 27 ++++++++++++++++++++---- tests/OpenApiBuildersTest.php | 22 +++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 tests/OpenApiBuildersTest.php diff --git a/docs/usage/access.md b/docs/usage/access.md index 14591861..9318321e 100644 --- a/docs/usage/access.md +++ b/docs/usage/access.md @@ -1,11 +1,11 @@ --- -title: Restricting access to docs +title: Docs authorization weight: 4 --- -The access to docs in non-production environment is enabled by default. +Scramble exposes docs at the `/docs/api` URI. By default, you will only be able to access this dashboard in the `local` environment. -If you need to allow access to docs in production environment, implement gate called `viewApiDocs`: +If you need to allow access to docs in `production` environment, implement gate called `viewApiDocs`: ```php Gate::define('viewApiDocs', function (User $user) { diff --git a/src/Support/Generator/OpenApi.php b/src/Support/Generator/OpenApi.php index 2926a013..26fdb35c 100644 --- a/src/Support/Generator/OpenApi.php +++ b/src/Support/Generator/OpenApi.php @@ -36,6 +36,16 @@ public function setComponents(Components $components) return $this; } + public function secure(SecurityScheme $securityScheme) + { + $this->components->addSecurityScheme($securityScheme->schemeName, $securityScheme); + if ($securityScheme->default) { + $this->defaultSecurity(new Security($securityScheme->schemeName)); + } + + return $this; + } + public function setInfo(InfoObject $info) { $this->info = $info; diff --git a/src/Support/Generator/SecurityScheme.php b/src/Support/Generator/SecurityScheme.php index 9fdeb307..80162cef 100644 --- a/src/Support/Generator/SecurityScheme.php +++ b/src/Support/Generator/SecurityScheme.php @@ -4,13 +4,17 @@ class SecurityScheme { - private string $type; + public string $type; - private string $name; + public string $name; - private string $in; + public string $in; - private string $description = ''; + public string $description = ''; + + public string $schemeName = 'scheme'; + + public bool $isDefault = false; private function __construct(string $type) { @@ -20,12 +24,20 @@ private function __construct(string $type) public static function apiKey(string $in, string $name) { $scheme = new self('apiKey'); + $scheme->schemeName = 'apiKey'; $scheme->in = $in; $scheme->name = $name; return $scheme; } + public function as(string $schemeName): SecurityScheme + { + $this->schemeName = $schemeName; + + return $this; + } + public function setDescription(string $description): SecurityScheme { $this->description = $description; @@ -33,6 +45,13 @@ public function setDescription(string $description): SecurityScheme return $this; } + public function default(): SecurityScheme + { + $this->default = true; + + return $this; + } + public function toArray() { return array_filter([ diff --git a/tests/OpenApiBuildersTest.php b/tests/OpenApiBuildersTest.php new file mode 100644 index 00000000..c8460990 --- /dev/null +++ b/tests/OpenApiBuildersTest.php @@ -0,0 +1,22 @@ +setInfo(InfoObject::make('API')->setVersion('0.0.1')); + + $openApi->secure(SecurityScheme::apiKey('query', 'api_token')->default()); + $document = $openApi->toArray(); + + expect($document['security'])->toBe([['apiKey' => []]]) + ->and($document['components']['securitySchemes'])->toBe([ + 'apiKey' => [ + 'type' => 'apiKey', + 'in' => 'query', + 'name' => 'api_token', + ] + ]); +}); From 10b0edd23c8133cd2799a6ee7bb50750419887f0 Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Tue, 20 Sep 2022 23:42:06 +0300 Subject: [PATCH 2/4] added other security types --- src/Generator.php | 2 +- src/Support/Generator/SecurityScheme.php | 46 +++++++++++------- .../SecuritySchemes/ApiKeySecurityScheme.php | 28 +++++++++++ .../SecuritySchemes/HttpSecurityScheme.php | 28 +++++++++++ .../Generator/SecuritySchemes/OAuthFlow.php | 47 ++++++++++++++++++ .../Generator/SecuritySchemes/OAuthFlows.php | 48 +++++++++++++++++++ .../SecuritySchemes/Oauth2SecurityScheme.php | 41 ++++++++++++++++ .../OpenIdConnectUrlSecurityScheme.php | 24 ++++++++++ tests/OpenApiBuildersTest.php | 20 ++++++++ ...t__it_builds_oauth2_security_scheme__1.yml | 8 ++++ 10 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 src/Support/Generator/SecuritySchemes/ApiKeySecurityScheme.php create mode 100644 src/Support/Generator/SecuritySchemes/HttpSecurityScheme.php create mode 100644 src/Support/Generator/SecuritySchemes/OAuthFlow.php create mode 100644 src/Support/Generator/SecuritySchemes/OAuthFlows.php create mode 100644 src/Support/Generator/SecuritySchemes/Oauth2SecurityScheme.php create mode 100644 src/Support/Generator/SecuritySchemes/OpenIdConnectUrlSecurityScheme.php create mode 100644 tests/__snapshots__/OpenApiBuildersTest__it_builds_oauth2_security_scheme__1.yml diff --git a/src/Generator.php b/src/Generator.php index 12907acb..17968059 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -40,7 +40,7 @@ public function __invoke() ->toArray(); if (isset(Scramble::$openApiExtender)) { - $openApi = (Scramble::$openApiExtender)($openApi); + (Scramble::$openApiExtender)($openApi); } return $openApi->toArray(); diff --git a/src/Support/Generator/SecurityScheme.php b/src/Support/Generator/SecurityScheme.php index 80162cef..2c88b044 100644 --- a/src/Support/Generator/SecurityScheme.php +++ b/src/Support/Generator/SecurityScheme.php @@ -2,50 +2,66 @@ namespace Dedoc\Scramble\Support\Generator; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\ApiKeySecurityScheme; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\HttpSecurityScheme; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\Oauth2SecurityScheme; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\OpenIdConnectUrlSecurityScheme; + class SecurityScheme { public string $type; - public string $name; - - public string $in; - public string $description = ''; public string $schemeName = 'scheme'; - public bool $isDefault = false; + public bool $default = false; - private function __construct(string $type) + public function __construct(string $type) { $this->type = $type; } public static function apiKey(string $in, string $name) { - $scheme = new self('apiKey'); - $scheme->schemeName = 'apiKey'; - $scheme->in = $in; - $scheme->name = $name; + return (new ApiKeySecurityScheme($in, $name))->as('apiKey'); + } + + public static function http(string $scheme, string $bearerFormat = '') + { + return (new HttpSecurityScheme($scheme, $bearerFormat))->as('http'); + } + + public static function oauth2() + { + return (new Oauth2SecurityScheme)->as('oauth2'); + } - return $scheme; + public static function openIdConnect(string $openIdConnectUrl) + { + return (new OpenIdConnectUrlSecurityScheme($openIdConnectUrl))->as('openIdConnect'); + } + + public static function mutualTLS() + { + return (new static('mutualTLS'))->as('mutualTLS'); } - public function as(string $schemeName): SecurityScheme + public function as(string $schemeName): self { $this->schemeName = $schemeName; return $this; } - public function setDescription(string $description): SecurityScheme + public function setDescription(string $description): self { $this->description = $description; return $this; } - public function default(): SecurityScheme + public function default(): self { $this->default = true; @@ -56,8 +72,6 @@ public function toArray() { return array_filter([ 'type' => $this->type, - 'in' => $this->in, - 'name' => $this->name, 'description' => $this->description, ]); } diff --git a/src/Support/Generator/SecuritySchemes/ApiKeySecurityScheme.php b/src/Support/Generator/SecuritySchemes/ApiKeySecurityScheme.php new file mode 100644 index 00000000..49069c26 --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/ApiKeySecurityScheme.php @@ -0,0 +1,28 @@ +in = $in; + $this->name = $name; + } + + public function toArray() + { + return array_merge(parent::toArray(), [ + 'in' => $this->in, + 'name' => $this->name, + ]); + } +} diff --git a/src/Support/Generator/SecuritySchemes/HttpSecurityScheme.php b/src/Support/Generator/SecuritySchemes/HttpSecurityScheme.php new file mode 100644 index 00000000..b81a3c97 --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/HttpSecurityScheme.php @@ -0,0 +1,28 @@ +scheme = $scheme; + $this->bearerFormat = $bearerFormat; + } + + public function toArray() + { + return array_merge(parent::toArray(), [ + 'scheme' => $this->scheme, + 'bearerFormat' => $this->bearerFormat, + ]); + } +} diff --git a/src/Support/Generator/SecuritySchemes/OAuthFlow.php b/src/Support/Generator/SecuritySchemes/OAuthFlow.php new file mode 100644 index 00000000..856e814c --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/OAuthFlow.php @@ -0,0 +1,47 @@ + */ + public array $scopes = []; + + public function authorizationUrl(string $authorizationUrl): OAuthFlow + { + $this->authorizationUrl = $authorizationUrl; + return $this; + } + + public function tokenUrl(string $tokenUrl): OAuthFlow + { + $this->tokenUrl = $tokenUrl; + return $this; + } + + public function refreshUrl(string $refreshUrl): OAuthFlow + { + $this->refreshUrl = $refreshUrl; + return $this; + } + + public function addScope(string $name, string $description = '') + { + $this->scopes[$name] = $description; + + return $this; + } + + public function toArray() + { + return array_filter([ + 'authorizationUrl' => $this->authorizationUrl, + 'tokenUrl' => $this->tokenUrl, + 'refreshUrl' => $this->refreshUrl, + 'scopes' => $this->scopes, + ]); + } +} diff --git a/src/Support/Generator/SecuritySchemes/OAuthFlows.php b/src/Support/Generator/SecuritySchemes/OAuthFlows.php new file mode 100644 index 00000000..3ace27b6 --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/OAuthFlows.php @@ -0,0 +1,48 @@ +implicit = $flow; + return $this; + } + + public function password(?OAuthFlow $flow): OAuthFlows + { + $this->password = $flow; + return $this; + } + + public function clientCredentials(?OAuthFlow $flow): OAuthFlows + { + $this->clientCredentials = $flow; + return $this; + } + + public function authorizationCode(?OAuthFlow $flow): OAuthFlows + { + $this->authorizationCode = $flow; + return $this; + } + + public function toArray() + { + return array_map( + fn ($f) => $f->toArray(), + array_filter([ + 'implicit' => $this->implicit, + 'password' => $this->password, + 'clientCredentials' => $this->clientCredentials, + 'authorizationCode' => $this->authorizationCode, + ]) + ); + } +} diff --git a/src/Support/Generator/SecuritySchemes/Oauth2SecurityScheme.php b/src/Support/Generator/SecuritySchemes/Oauth2SecurityScheme.php new file mode 100644 index 00000000..23e50474 --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/Oauth2SecurityScheme.php @@ -0,0 +1,41 @@ +oAuthFlows = new OAuthFlows; + } + + public function flows(callable $flows) + { + $flows($this->oAuthFlows); + + return $this; + } + + public function flow(string $name, callable $flow) + { + return $this->flows(function (OAuthFlows $flows) use ($flow, $name) { + if (! $flows->$name) { + $flows->$name(new OAuthFlow); + } + $flow($flows->$name); + }); + } + + public function toArray() + { + return array_merge(parent::toArray(), [ + 'flows' => $this->oAuthFlows->toArray(), + ]); + } +} diff --git a/src/Support/Generator/SecuritySchemes/OpenIdConnectUrlSecurityScheme.php b/src/Support/Generator/SecuritySchemes/OpenIdConnectUrlSecurityScheme.php new file mode 100644 index 00000000..b8c238eb --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/OpenIdConnectUrlSecurityScheme.php @@ -0,0 +1,24 @@ +openIdConnectUrl = $openIdConnectUrl; + } + + public function toArray() + { + return array_merge(parent::toArray(), [ + 'openIdConnectUrl' => $this->openIdConnectUrl, + ]); + } +} diff --git a/tests/OpenApiBuildersTest.php b/tests/OpenApiBuildersTest.php index c8460990..f7a7a920 100644 --- a/tests/OpenApiBuildersTest.php +++ b/tests/OpenApiBuildersTest.php @@ -3,6 +3,8 @@ use Dedoc\Scramble\Support\Generator\InfoObject; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; +use function Spatie\Snapshots\assertMatchesSnapshot; it('builds security scheme', function () { $openApi = (new OpenApi('3.1.0')) @@ -20,3 +22,21 @@ ] ]); }); + +it('builds oauth2 security scheme', function () { + $openApi = (new OpenApi('3.1.0')) + ->setInfo(InfoObject::make('API')->setVersion('0.0.1')); + + $openApi->secure( + SecurityScheme::oauth2() + ->flow('implicit', function (OAuthFlow $flow) { + $flow + ->refreshUrl('https://test.com') + ->tokenUrl('https://test.com/token') + ->addScope('wow', 'nice'); + }) + ->default() + ); + + assertMatchesSnapshot($openApi->toArray()); +}); diff --git a/tests/__snapshots__/OpenApiBuildersTest__it_builds_oauth2_security_scheme__1.yml b/tests/__snapshots__/OpenApiBuildersTest__it_builds_oauth2_security_scheme__1.yml new file mode 100644 index 00000000..23a4c6ca --- /dev/null +++ b/tests/__snapshots__/OpenApiBuildersTest__it_builds_oauth2_security_scheme__1.yml @@ -0,0 +1,8 @@ +openapi: 3.1.0 +info: + title: API + version: 0.0.1 +security: + - { oauth2: { } } +components: + securitySchemes: { oauth2: { type: oauth2, flows: { implicit: { tokenUrl: 'https://test.com/token', refreshUrl: 'https://test.com', scopes: { wow: nice } } } } } From 338a0e679f35e8835db9c97878f6f77fc5be7d5b Mon Sep 17 00:00:00 2001 From: romalytvynenko Date: Tue, 20 Sep 2022 20:42:44 +0000 Subject: [PATCH 3/4] Fix styling --- src/Support/Generator/SecuritySchemes/OAuthFlow.php | 6 ++++++ src/Support/Generator/SecuritySchemes/OAuthFlows.php | 7 +++++++ tests/OpenApiBuildersTest.php | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Support/Generator/SecuritySchemes/OAuthFlow.php b/src/Support/Generator/SecuritySchemes/OAuthFlow.php index 856e814c..035ca09c 100644 --- a/src/Support/Generator/SecuritySchemes/OAuthFlow.php +++ b/src/Support/Generator/SecuritySchemes/OAuthFlow.php @@ -5,26 +5,32 @@ class OAuthFlow { public string $authorizationUrl = ''; + public string $tokenUrl = ''; + public string $refreshUrl = ''; + /** @var array */ public array $scopes = []; public function authorizationUrl(string $authorizationUrl): OAuthFlow { $this->authorizationUrl = $authorizationUrl; + return $this; } public function tokenUrl(string $tokenUrl): OAuthFlow { $this->tokenUrl = $tokenUrl; + return $this; } public function refreshUrl(string $refreshUrl): OAuthFlow { $this->refreshUrl = $refreshUrl; + return $this; } diff --git a/src/Support/Generator/SecuritySchemes/OAuthFlows.php b/src/Support/Generator/SecuritySchemes/OAuthFlows.php index 3ace27b6..b4b50e33 100644 --- a/src/Support/Generator/SecuritySchemes/OAuthFlows.php +++ b/src/Support/Generator/SecuritySchemes/OAuthFlows.php @@ -5,31 +5,38 @@ class OAuthFlows { public ?OAuthFlow $implicit = null; + public ?OAuthFlow $password = null; + public ?OAuthFlow $clientCredentials = null; + public ?OAuthFlow $authorizationCode = null; public function implicit(?OAuthFlow $flow): OAuthFlows { $this->implicit = $flow; + return $this; } public function password(?OAuthFlow $flow): OAuthFlows { $this->password = $flow; + return $this; } public function clientCredentials(?OAuthFlow $flow): OAuthFlows { $this->clientCredentials = $flow; + return $this; } public function authorizationCode(?OAuthFlow $flow): OAuthFlows { $this->authorizationCode = $flow; + return $this; } diff --git a/tests/OpenApiBuildersTest.php b/tests/OpenApiBuildersTest.php index f7a7a920..bf46ed43 100644 --- a/tests/OpenApiBuildersTest.php +++ b/tests/OpenApiBuildersTest.php @@ -19,7 +19,7 @@ 'type' => 'apiKey', 'in' => 'query', 'name' => 'api_token', - ] + ], ]); }); From b149af0c0de894713c0e6d9fb7a134d0cd360dba Mon Sep 17 00:00:00 2001 From: Roman Lytvynenko Date: Wed, 21 Sep 2022 00:07:38 +0300 Subject: [PATCH 4/4] documentation for security --- docs/usage/access.md | 4 +- docs/usage/security.md | 61 +++++++++++++++++++++++++++++++ src/Support/Generator/OpenApi.php | 2 + tests/OpenApiBuildersTest.php | 3 +- 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 docs/usage/security.md diff --git a/docs/usage/access.md b/docs/usage/access.md index 9318321e..9105c038 100644 --- a/docs/usage/access.md +++ b/docs/usage/access.md @@ -1,9 +1,9 @@ --- title: Docs authorization -weight: 4 +weight: 5 --- -Scramble exposes docs at the `/docs/api` URI. By default, you will only be able to access this dashboard in the `local` environment. +Scramble exposes docs at the `/docs/api` URI. By default, you will only be able to access this route in the `local` environment. If you need to allow access to docs in `production` environment, implement gate called `viewApiDocs`: diff --git a/docs/usage/security.md b/docs/usage/security.md new file mode 100644 index 00000000..58c5f58b --- /dev/null +++ b/docs/usage/security.md @@ -0,0 +1,61 @@ +--- +title: Security +weight: 4 +--- + +Most likely, your API is protected by some sort of the auth. OpenAPI have plenty of ways to describe the API (see full documentation of spec here https://spec.openapis.org/oas/v3.1.0#security-scheme-object). + +## Adding security scheme +Scramble allows you to document how your API is secured. To document this, you can use `Scramble::extendOpenApi` method and add security information to OpenAPI document using `secure` method. + +You should call `extendOpenApi` in `register` method of some of your service providers. This method accepts a callback that accepts OpenAPI document as a first argument. + +`secure` method on `OpenApi` object accepts security scheme as an argument. It makes the security scheme default for all endpoints. + +```php +namespace App\Providers; + +use Dedoc\Scramble\Support\Generator\OpenApi; +use Dedoc\Scramble\Support\Generator\SecurityScheme; +use Illuminate\Support\ServiceProvider; + +class DocsServiceProvider extends ServiceProvider +{ + public function register() + { + Scramble::extendOpenApi(function (OpenApi $openApi) { + $openApi->secure( + SecurityScheme::apiKey('query', 'api_token') + ); + }); + } +} +``` + +## Examples +Here are some common examples of the security schemes object you may have in your API. For full list of available methods check implementation of `SecurityScheme` class. + +### API Key +```php +SecurityScheme::apiKey('query', 'api_token'); +``` + +### Basic HTTP +```php +SecurityScheme::http('basic'); +``` + +### JWT +```php +SecurityScheme::http('bearer', 'JWT'); +``` + +### Oauth2 +```php +SecurityScheme::oauth2() + ->flow('implicit', function (OAuthFlow $flow) { + $flow + ->authorizationUrl('https://example.com/api/oauth/dialog') + ->addScope('write:pets', 'modify pets in your account'); + }); +``` diff --git a/src/Support/Generator/OpenApi.php b/src/Support/Generator/OpenApi.php index 26fdb35c..bf3610db 100644 --- a/src/Support/Generator/OpenApi.php +++ b/src/Support/Generator/OpenApi.php @@ -38,6 +38,8 @@ public function setComponents(Components $components) public function secure(SecurityScheme $securityScheme) { + $securityScheme->default(); + $this->components->addSecurityScheme($securityScheme->schemeName, $securityScheme); if ($securityScheme->default) { $this->defaultSecurity(new Security($securityScheme->schemeName)); diff --git a/tests/OpenApiBuildersTest.php b/tests/OpenApiBuildersTest.php index f7a7a920..1e8b2a4e 100644 --- a/tests/OpenApiBuildersTest.php +++ b/tests/OpenApiBuildersTest.php @@ -10,7 +10,7 @@ $openApi = (new OpenApi('3.1.0')) ->setInfo(InfoObject::make('API')->setVersion('0.0.1')); - $openApi->secure(SecurityScheme::apiKey('query', 'api_token')->default()); + $openApi->secure(SecurityScheme::apiKey('query', 'api_token')); $document = $openApi->toArray(); expect($document['security'])->toBe([['apiKey' => []]]) @@ -35,7 +35,6 @@ ->tokenUrl('https://test.com/token') ->addScope('wow', 'nice'); }) - ->default() ); assertMatchesSnapshot($openApi->toArray());