File Coverage

blib/lib/Mojolicious/Plugin/OpenAPI/Security.pm
Criterion Covered Total %
statement 53 53 100.0
branch 28 30 93.3
condition 11 16 68.7
subroutine 5 5 100.0
pod 1 1 100.0
total 98 105 93.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::OpenAPI::Security;
2 48     48   330 use Mojo::Base -base;
  48         102  
  48         342  
3              
4             my %DEF_PATH
5             = ('openapiv2' => '/securityDefinitions', 'openapiv3' => '/components/securitySchemes');
6              
7             sub register {
8 59     59 1 700 my ($self, $app, $config) = @_;
9 59         170 my $openapi = $config->{openapi};
10 59 100       433 my $handlers = $config->{security} or return;
11              
12 3 50       12 return unless $openapi->validator->get($DEF_PATH{$openapi->validator->moniker});
13 3         343 return $openapi->route(
14             $openapi->route->under('/')->to(cb => $self->_build_action($openapi, $handlers)));
15             }
16              
17             sub _build_action {
18 3     3   514 my ($self, $openapi, $handlers) = @_;
19 3   50     9 my $global = $openapi->validator->get('/security') || [];
20 3         223 my $definitions = $openapi->validator->get($DEF_PATH{$openapi->validator->moniker});
21              
22             return sub {
23 28     28   313843 my $c = shift;
24 28 100 100     96 return 1 if $c->req->method eq 'OPTIONS' and $c->match->stack->[-1]{'openapi.default_options'};
25              
26 26   50     450 my $spec = $c->openapi->spec || {};
27 26 100       3952 my @security_or = @{$spec->{security} || $global};
  26         148  
28 26         86 my ($sync_mode, $n_checks, %res) = (1, 0);
29              
30             my $security_completed = sub {
31 24         59 my ($i, $status, @errors) = (0, 401);
32              
33             SECURITY_AND:
34 24         45 for my $security_and (@security_or) {
35 32         52 my @e;
36              
37 32         91 for my $name (sort keys %$security_and) {
38 44         101 my $error_path = sprintf '/security/%s/%s', $i, _pointer_escape($name);
39             push @e, ref $res{$name} ? $res{$name} : {message => $res{$name}, path => $error_path}
40 44 100       218 if defined $res{$name};
    100          
41             }
42              
43             # Authenticated
44             # Cannot call $c->continue() in case this callback was called
45             # synchronously, since it will result in an infinite loop.
46 32 100       86 unless (@e) {
47 13 100 66     30 return if eval { $sync_mode || $c->continue || 1 };
  13 100       83  
48 2         48 chomp $@;
49 2         9 $c->app->log->error($@);
50 2         42 @errors = ({message => 'Internal Server Error.', path => '/'});
51 2         5 $status = 500;
52 2         8 last SECURITY_AND;
53             }
54              
55             # Not authenticated
56 19         42 push @errors, @e;
57 19         40 $i++;
58             }
59 13 100 66     74 $status = $c->stash('status') || $status if $status < 500;
60 13         185 $c->render(openapi => {errors => \@errors}, status => $status);
61 13         9717 $n_checks = -1; # Make sure we don't render twice
62 26         144 };
63              
64 26         72 for my $security_and (@security_or) {
65 34         727 for my $name (sort keys %$security_and) {
66 46         690 my $security_cb = $handlers->{$name};
67              
68 46 100       156 if (!$security_cb) {
    100          
69 4 50       21 $res{$name} = {message => "No security callback for $name."} unless exists $res{$name};
70             }
71             elsif (!exists $res{$name}) {
72 40         96 $res{$name} = undef;
73 40         57 $n_checks++;
74              
75             # $security_cb is obviously called synchronously, but the callback
76             # might also be called synchronously. We need the $sync_mode guard
77             # to make sure that we do not call continue() if that is the case.
78             $c->$security_cb(
79             $definitions->{$name},
80             $security_and->{$name},
81             sub {
82 38   66     3413 $res{$name} //= $_[1];
83 38 100       166 $security_completed->() if --$n_checks == 0;
84             }
85 40         247 );
86             }
87             }
88             }
89              
90             # If $security_completed was called already, then $n_checks will zero and
91             # we return "1" which means we are in synchronous mode. When running async,
92             # we need to asign undef() to $sync_mode, since it is used inside
93             # $security_completed to call $c->continue()
94 24 100       179 return $sync_mode = $n_checks ? undef : 1;
95 3         267 };
96             }
97              
98 44     44   75 sub _pointer_escape { local $_ = shift; s/~/~0/g; s!/!~1!g; $_; }
  44         96  
  44         74  
  44         247  
99              
100             1;
101              
102             =encoding utf8
103              
104             =head1 NAME
105              
106             Mojolicious::Plugin::OpenAPI::Security - OpenAPI plugin for securing your API
107              
108             =head1 DESCRIPTION
109              
110             This plugin will allow you to use the security features provided by the OpenAPI
111             specification.
112              
113             Note that this is currently EXPERIMENTAL! Please let me know if you have any
114             feedback. See L for a
115             complete discussion.
116              
117             =head1 TUTORIAL
118              
119             =head2 Specification
120              
121             Here is an example specification that use
122             L
123             and L from
124             the OpenAPI spec:
125              
126             {
127             "swagger": "2.0",
128             "info": { "version": "0.8", "title": "Super secure" },
129             "schemes": [ "https" ],
130             "basePath": "/api",
131             "securityDefinitions": {
132             "dummy": {
133             "type": "apiKey",
134             "name": "Authorization",
135             "in": "header",
136             "description": "dummy"
137             }
138             },
139             "paths": {
140             "/protected": {
141             "post": {
142             "x-mojo-to": "super#secret_resource",
143             "security": [{"dummy": []}],
144             "parameters": [
145             { "in": "body", "name": "body", "schema": { "type": "object" } }
146             ],
147             "responses": {
148             "200": {"description": "Echo response", "schema": { "type": "object" }},
149             "401": {"description": "Sorry mate", "schema": { "type": "array" }}
150             }
151             }
152             }
153             }
154             }
155              
156             =head2 Application
157              
158             The specification above can be dispatched to handlers inside your
159             L application. The do so, add the "security" key when loading the
160             plugin, and reference the "securityDefinitions" name inside that to a callback.
161             In this example, we have the "dummy" security handler:
162              
163             package Myapp;
164             use Mojo::Base "Mojolicious";
165              
166             sub startup {
167             my $app = shift;
168              
169             $app->plugin(OpenAPI => {
170             url => "data:///security.json",
171             security => {
172             dummy => sub {
173             my ($c, $definition, $scopes, $cb) = @_;
174             return $c->$cb() if $c->req->headers->authorization;
175             return $c->$cb('Authorization header not present');
176             }
177             }
178             });
179             }
180              
181             1;
182              
183             C<$c> is a L object. C<$definition> is the security
184             definition from C. C<$scopes> is the Oauth scopes, which
185             in this case is just an empty array ref, but it will contain the value for
186             "security" under the given HTTP method.
187              
188             Call C<$cb> with C or no argument at all to indicate pass. Call C<$cb>
189             with a defined value (usually a string) to indicate that the check has failed.
190             When none of the sets of security restrictions are satisfied, the standard
191             OpenAPI structure is built using the values passed to the callbacks as the
192             messages and rendered to the client with a status of 401.
193              
194             Note that the callback must be called or the dispatch will hang.
195              
196             See also L for example
197             L application.
198              
199             =head2 Controller
200              
201             Your controllers and actions are unchanged. The difference in behavior is that
202             the action simply won't be called if you fail to pass the security tests.
203              
204             =head2 Exempted routes
205              
206             All of the routes created by the plugin are protected by the security
207             definitions with the following exemptions. The base route that renders the
208             spec/documentation is exempted. Additionally, when a route does not define its
209             own C handler a documentation endpoint is generated which is exempt as
210             well.
211              
212             =head1 METHODS
213              
214             =head2 register
215              
216             Called by L.
217              
218             =head1 SEE ALSO
219              
220             L.
221              
222             =cut