diff --git a/docs/usage/access.md b/docs/usage/access.md index 14591861..9105c038 100644 --- a/docs/usage/access.md +++ b/docs/usage/access.md @@ -1,11 +1,11 @@ --- -title: Restricting access to docs -weight: 4 +title: Docs authorization +weight: 5 --- -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 route 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/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/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/OpenApi.php b/src/Support/Generator/OpenApi.php index 2926a013..bf3610db 100644 --- a/src/Support/Generator/OpenApi.php +++ b/src/Support/Generator/OpenApi.php @@ -36,6 +36,18 @@ public function setComponents(Components $components) return $this; } + public function secure(SecurityScheme $securityScheme) + { + $securityScheme->default(); + + $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..2c88b044 100644 --- a/src/Support/Generator/SecurityScheme.php +++ b/src/Support/Generator/SecurityScheme.php @@ -2,43 +2,76 @@ 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 { - private string $type; + public string $type; - private string $name; + public string $description = ''; - private string $in; + public string $schemeName = 'scheme'; - private string $description = ''; + 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->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'); + } + + 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): self + { + $this->schemeName = $schemeName; - return $scheme; + return $this; } - public function setDescription(string $description): SecurityScheme + public function setDescription(string $description): self { $this->description = $description; return $this; } + public function default(): self + { + $this->default = true; + + return $this; + } + 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..035ca09c --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/OAuthFlow.php @@ -0,0 +1,53 @@ + */ + 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..b4b50e33 --- /dev/null +++ b/src/Support/Generator/SecuritySchemes/OAuthFlows.php @@ -0,0 +1,55 @@ +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 new file mode 100644 index 00000000..220b65f6 --- /dev/null +++ b/tests/OpenApiBuildersTest.php @@ -0,0 +1,41 @@ +setInfo(InfoObject::make('API')->setVersion('0.0.1')); + + $openApi->secure(SecurityScheme::apiKey('query', 'api_token')); + $document = $openApi->toArray(); + + expect($document['security'])->toBe([['apiKey' => []]]) + ->and($document['components']['securitySchemes'])->toBe([ + 'apiKey' => [ + 'type' => 'apiKey', + 'in' => 'query', + 'name' => 'api_token', + ], + ]); +}); + +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'); + }) + ); + + 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 } } } } }